├── .github
├── FUNDING.yml
└── workflows
│ ├── backend.yml
│ ├── frontend.yml
│ └── nohup.out
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── SpeedSolverBackend
└── SpeedSolverAPI
│ ├── .env.example
│ ├── Makefile
│ ├── alembic.ini
│ ├── alembic
│ ├── README
│ ├── env.py
│ ├── script.py.mako
│ └── versions
│ │ ├── 09ac58fca3de_.py
│ │ ├── 29f26748a5e4_.py
│ │ ├── 36551275d3fe_.py
│ │ ├── d911c2926905_.py
│ │ └── e238014fade3_.py
│ ├── app
│ ├── cfg
│ │ └── settings.py
│ ├── database
│ │ ├── abstract
│ │ │ └── abc_repo.py
│ │ ├── database.py
│ │ ├── models
│ │ │ └── models.py
│ │ └── repo
│ │ │ ├── organization_repository.py
│ │ │ ├── project_invitation_repository.py
│ │ │ ├── project_members_repository.py
│ │ │ ├── project_repository.py
│ │ │ ├── team_invitation_repository.py
│ │ │ ├── team_member_repository.py
│ │ │ ├── team_project_repository.py
│ │ │ ├── team_repository.py
│ │ │ ├── user_profile_repository.py
│ │ │ ├── user_repository.py
│ │ │ └── verification_repository.py
│ ├── exc
│ │ └── bad_email.py
│ ├── routing
│ │ ├── access_router.py
│ │ ├── account_router.py
│ │ ├── main_router.py
│ │ ├── organization_router.py
│ │ ├── project_router.py
│ │ ├── team
│ │ │ ├── team_invites_router.py
│ │ │ └── team_router.py
│ │ └── verification_router.py
│ ├── schema
│ │ ├── request
│ │ │ ├── account
│ │ │ │ └── updateprofile.py
│ │ │ ├── email
│ │ │ │ ├── confirm_email.py
│ │ │ │ └── resend_code.py
│ │ │ ├── get_access
│ │ │ │ ├── authorize.py
│ │ │ │ └── register.py
│ │ │ ├── objective
│ │ │ │ └── create_objective.py
│ │ │ ├── organization
│ │ │ │ ├── create_organization.py
│ │ │ │ └── update_organization.py
│ │ │ ├── project
│ │ │ │ ├── create_project.py
│ │ │ │ └── update_project.py
│ │ │ └── team
│ │ │ │ ├── create_team.py
│ │ │ │ ├── invites
│ │ │ │ └── invite_user.py
│ │ │ │ └── update_team.py
│ │ └── response
│ │ │ ├── AccessToken.py
│ │ │ ├── leader
│ │ │ └── read_leader.py
│ │ │ └── team
│ │ │ ├── read_team.py
│ │ │ └── team.py
│ ├── security
│ │ ├── hasher.py
│ │ ├── jwtmanager.py
│ │ └── jwttype.py
│ ├── services
│ │ ├── organization_service.py
│ │ ├── project_service.py
│ │ ├── team_invitation_service.py
│ │ ├── team_project_service.py
│ │ ├── team_service.py
│ │ ├── user_profile_service.py
│ │ ├── user_service.py
│ │ └── verification_service.py
│ └── utils
│ │ ├── email_service
│ │ └── email_service.py
│ │ ├── logger
│ │ ├── logger.py
│ │ └── telegram_bot
│ │ │ └── telegram_logger.py
│ │ ├── result.py
│ │ └── verify_codes_generator
│ │ └── code_generator.py
│ ├── docker
│ ├── Dockerfile.backend
│ └── docker-compose.backend.yml
│ ├── main.py
│ ├── poetry.lock
│ ├── pyproject.toml
│ └── tests
│ └── telegram_logger_test.py
└── SpeedSolverFrontend
├── .env.example
├── .gitignore
├── README.md
├── components.json
├── docker
├── Dockerfile.frontend
└── docker-compose.frontend.yml
├── eslint.config.js
├── index.html
├── nginx.conf
├── package-lock.json
├── package.json
├── public
└── vite.svg
├── src
├── App.css
├── App.tsx
├── app
│ └── axios_api.ts
├── assets
│ ├── mainbackground.svg
│ └── react.svg
├── components
│ ├── card
│ │ └── Card.tsx
│ ├── dashboardNavigation
│ │ ├── dashboardNavigation.css
│ │ └── dashboardNavigation.tsx
│ ├── passwordInput
│ │ ├── PasswordInput.module.css
│ │ └── PasswordInput.tsx
│ ├── swappingText
│ │ ├── SwappingText.tsx
│ │ └── TextStyles.css
│ └── teamslist
│ │ ├── TeamsList.css
│ │ └── TeamsList.tsx
├── index.css
├── layout
│ └── DashboardLayout.tsx
├── lib
│ └── utils.ts
├── main.tsx
├── pages
│ ├── aboutUs
│ │ └── aboutPage.tsx
│ ├── access
│ │ ├── login
│ │ │ └── LoginPage.tsx
│ │ └── register
│ │ │ └── RegisterPage.tsx
│ ├── dashboard
│ │ ├── Companies.tsx
│ │ ├── Dashboard.tsx
│ │ ├── Projects.tsx
│ │ └── Teams.tsx
│ ├── teams
│ │ ├── TeamsPage.css
│ │ └── TeamsPage.tsx
│ ├── test
│ │ └── TestPage.tsx
│ └── welcome
│ │ ├── WelcomePage.module.css
│ │ └── WelcomePage.tsx
├── types
│ └── teams
│ │ └── types.ts
├── utils
│ └── cookieUtils.ts
└── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: # nvm :)
2 |
--------------------------------------------------------------------------------
/.github/workflows/backend.yml:
--------------------------------------------------------------------------------
1 | name: Build Backend
2 |
3 | on:
4 | push:
5 | branches:
6 | - backend
7 | - master
8 | pull_request:
9 | branches:
10 | - develop
11 | - master
12 |
13 |
14 | jobs:
15 | Deploy:
16 | runs-on: self-hosted
17 | steps:
18 | - name: Downloading repository
19 | uses: actions/checkout@v4
20 |
21 | - name: Copy .env backend file
22 | run: |
23 | cd SpeedSolverBackend/SpeedSolverAPI
24 | echo "${{ secrets.ENV_SPEEDSOLVER }}" > .env
25 |
26 | - name: Run Backend
27 | run: |
28 | make build-backend
29 |
--------------------------------------------------------------------------------
/.github/workflows/frontend.yml:
--------------------------------------------------------------------------------
1 | name: Deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - frontend
7 | - master
8 | pull_request:
9 | branches:
10 | - develop
11 | - master
12 |
13 |
14 | jobs:
15 | Deploy:
16 | runs-on: self-hosted
17 | steps:
18 | - name: Downloading repository
19 | uses: actions/checkout@v4
20 |
21 | - name: Copy .env frontend file
22 | run: |
23 | cd SpeedSolverFrontend
24 | echo "${{ secrets.ENV_SPEEDSOLVER_FRONTEND }}" > .env
25 |
26 | - name: Run Frontend
27 | run: |
28 | make build-frontend
29 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/nohup.out:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w1tnessbtwwwww/SpeedSolver/4c34a55114e36d61088c2b18fcd395c72833425f/.github/workflows/nohup.out
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .vs
3 | .idea
4 |
5 | node_modules/
6 | logs/
7 | __pycache__/
8 | SpeedSolverAPI/docker/docker-compose-dev.yml
9 | .vscode/
10 | .env.develop
11 | *.pyc
12 | *.pyo
13 | *.pyd
14 | *.log
15 |
16 | nohup.out
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Кодекс поведения
2 |
3 | ## Наше сообщество
4 |
5 | Мы стремимся создать уважительное и безопасное сообщество, где каждый участник чувствует себя комфортно и может свободно выражать свои мысли. Мы приветствуем разнообразие и включенность, и ожидаем, что все участники будут придерживаться этих принципов.
6 |
7 | ## Наши ожидания
8 |
9 | Участники нашего сообщества должны придерживаться следующих правил:
10 |
11 | 1. **Уважение**: Уважайте всех участников сообщества, независимо от их происхождения, пола, возраста, сексуальной ориентации, религии, инвалидности или любых других характеристик.
12 | 2. **Конструктивность**: Вносите конструктивные и полезные комментарии. Избегайте личных нападок и оскорблений.
13 | 3. **Сотрудничество**: Сотрудничайте с другими участниками и стремитесь к совместному решению проблем.
14 | 4. **Ответственность**: Признавайте свои ошибки и стремитесь к их исправлению.
15 | 5. **Профессионализм**: Ведите себя профессионально и этично.
16 |
17 | ## Недопустимое поведение
18 |
19 | Недопустимое поведение включает, но не ограничивается:
20 |
21 | 1. **Оскорбления**: Личные нападки, оскорбления, угрозы или любые другие формы агрессивного поведения.
22 | 2. **Дискриминация**: Любые формы дискриминации по признаку расы, пола, возраста, сексуальной ориентации, религии, инвалидности или любых других характеристик.
23 | 3. **Домогательства**: Сексуальные домогательства, преследования или любые другие формы нежелательного внимания.
24 | 4. **Неуважение**: Неуважительное поведение, включая прерывание, игнорирование или отказ от сотрудничества.
25 | 5. **Нарушение конфиденциальности**: Раскрытие личной информации без согласия.
26 |
27 | ## Ответственность
28 |
29 | Мы ожидаем, что все участники сообщества будут придерживаться этого кодекса поведения. В случае нарушения этих правил, мы оставляем за собой право принять соответствующие меры, включая, но не ограничиваясь, предупреждение, временное или постоянное исключение из сообщества.
30 |
31 | ## Сообщение о нарушениях
32 |
33 | Если вы стали свидетелем или подверглись недопустимому поведению, пожалуйста, сообщите об этом одному из администраторов проекта. Мы обязуемся рассмотреть все сообщения конфиденциально и принять соответствующие меры.
34 |
35 | ## Контактная информация
36 |
37 | Для сообщений о нарушениях или вопросов по кодексу поведения, пожалуйста, свяжитесь с нами по следующим контактам:
38 |
39 | - [w1tnessbtwwwww] ([mailto:work.w1tnessbtw@mail.ru])
40 |
41 | ## Лицензия
42 |
43 | Этот кодекс поведения основан на [Contributor Covenant](https://www.contributor-covenant.org/), версия 2.0, доступная по адресу [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2024 Surovtsev Alexey
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build-backend:
2 | docker-compose --env-file SpeedSolverBackend/SpeedSolverAPI/.env -f SpeedSolverBackend/SpeedSolverAPI/docker/docker-compose.backend.yml --project-directory SpeedSolverBackend/SpeedSolverAPI up --build -d
3 |
4 | build-frontend:
5 | docker-compose -f SpeedSolverFrontend/docker/docker-compose.frontend.yml --project-directory SpeedSolverFrontend up --build -d
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
SPEEDSOLVER
2 |
3 | ***SPEEDSOLVER*** — это система управления проектами, предназначенная для эффективного управления командами, проектами, задачами, подзадачами и дедлайнами. Проект помогает командам организовать свою работу, отслеживать прогресс и достигать поставленных целей в срок.
4 |
5 | ## Важно!
6 | > [!IMPORTANT]
7 | > Проект является опенсорсным решением для вашей команды, и ссылки, которые предоставлены выше, неспособны выдержать чрезмерно высокую нагрузку.
8 |
9 |
10 | > [!IMPORTANT]
11 | > - https://speedsolver.ru/ - основной сайт.
12 | > - https://api.speedsolver.ru/docs - документация к API
13 |
14 |
15 |
16 | ## Содержание
17 |
18 | - [Описание](#описание)
19 | - [Функции](#функции)
20 | - [Стек технологий](#стек-технологий)
21 | - [Установка и запуск](#установка)
22 | - [Лицензия](#лицензия)
23 |
24 | ## Функции
25 |
26 | - [X] **Авторизация**: авторизация в рамках сессий благодаря JWT токенам. Предоставление access и refresh токенов.
27 | - [X] **Регистрация через почту**: Регистрация, благодаря подтверждению почты через код.
28 | - [ ] **Личный профиль**: Информация о Вас, социальные сети.
29 | - [ ] **Личная статистика**: Количество открытых и закрытых задач.
30 | - [X] **Управление командами**: Создание, удаление, обновление команды.
31 | - [X] **Менеджмент участников команды**: Приглашение в команду, удаление из команды
32 | - [X] **Назначение модераторов в команде**: Позволять модераторам создавать проекты для команды.
33 | - [X] **Управление проектами**: Создание, обновление, удаление проектов. Общение в реальном времени внутри проекта с сохранением истории чата.
34 | - [X] **Управление задачами**: Создание, редактирование и удаление задач.
35 | - [ ] **Дедлайны**: Установка и отслеживание дедлайнов для задач и подзадач.
36 | - [ ] **Уведомления**: Автоматические уведомления о приближающихся дедлайнах и изменениях в задачах.
37 |
38 | ## Стек технологий
39 |
40 | - **Frontend**: React + TypeScript
41 | - **Backend**: Python - FastAPI, Pydantic, Uvicorn (проксирующийся через nginx)
42 | - **Object Relational Mapping**: Python SQLAlchemy, Alembic, asyncpg driver
43 | - **База данных**: PostgreSQL
44 | - **Аутентификация & Авторизация**: JWT (JSON Web Tokens) - pyjwt
45 | - **Тестирование Backend**: Python - pytest
46 | - **Мониторинг**: Grafana, Prometheus, Node Exporter, AlertManager
47 | - **Логирование**: Собственная библиотека [teleglog](https://github.com/w1tnessbtwwwww/teleglog)
48 | - **Дополнительно**:
49 | - Docker – контейнеризация приложения.
50 | - Nginx - Веб-сервер для проксирования внешних подключений путем **reverse proxy**, реализация защищенного соединения с **SSL/TLS**, проксирование поддоменов.
51 | - Github Actions (CI/CD) – непрерывная интеграция и непрерывная доставка.
52 |
53 | ## Установка и запуск
54 |
55 | 1. Клонируйте репозиторий:
56 | ```shell
57 | git clone https://github.com/w1tnessbtwwwww/SpeedSolver.git
58 | 2. Необходимо создать файл **.env** и заполнить его из **.env.example**.
59 | 3. Необходимо переназначить порты в файле **.env** в случае, если у вас заняты одни из назначенных вами.
60 | 4. Запустите Docker Engine на вашем компьютере или виртуальной машине.
61 | 5. Запустите контейнеры Docker с **Backend** и **Frontend** частью:
62 | - macOS/Linux:
63 | ```shell
64 | make build-backend
65 | make build-frontend
66 | ```
67 |
68 | - Windows:
69 | - При наличии утилиты **make** от **chocolatey**:
70 | ```shell
71 | make build-backend
72 | make build-frontend
73 | ```
74 | - В ином случае:
75 | ```shell
76 | docker-compose --env-file SpeedSolverBackend/SpeedSolverAPI/.env -f SpeedSolverBackend/SpeedSolverAPI/docker/docker-compose.backend.yml --project-directory SpeedSolverBackend/SpeedSolverAPI up --build -d
77 |
78 | docker-compose -f docker/docker-compose.frontend.yml --project-directory . up --build
79 | ```
80 | После выполнения всех вышеперечисленных шагов вы сможете получить доступ к:
81 | - Backend - http://localhost:port/docs, запросы - https://localhost:port/v1/...
82 | - Frontend - http://localhost:port/
83 |
84 | ## Лицензия
85 | Этот проект лицензирован по лицензии Apache License 2.0. Подробности см. в файле [LICENSE](https://github.com/w1tnessbtwwwww/SpeedSolver/blob/master/LICENSE).
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Политика безопасности
2 |
3 | ## Поддерживаемые версии
4 |
5 |
6 | | Версия | Поддерживается |
7 | | ------ | ------------------- |
8 | | none | :white_check_mark: |
9 |
10 |
11 | ## Сообщение об уязвимости
12 |
13 | Если вы считаете, что нашли уязвимость безопасности в этом проекте, пожалуйста, сообщите нам, следуя этим шагам:
14 |
15 | 1. **Не открывайте issue**: Для обеспечения безопасности наших пользователей, пожалуйста, не открывайте публичный issue на GitHub.
16 |
17 | 2. **Отправьте электронное письмо**: Отправьте электронное письмо на [work.w1tnessbtw@mail.ru](mailto:work.w1tnessbtw@mail.ru) с следующей информацией:
18 | - Подробное описание уязвимости.
19 | - Шаги для воспроизведения уязвимости.
20 | - Любые соответствующие фрагменты кода или скриншоты.
21 | - Вашу контактную информацию (если вы хотите быть упомянутым за обнаружение).
22 |
23 | 3. **Дождитесь ответа**: Наша команда безопасности рассмотрит ваш отчет и свяжется с вами как можно скорее.
24 |
25 | ## Зал славы безопасности
26 |
27 | Мы ценим усилия исследователей безопасности, которые ответственно сообщают нам об уязвимостях. Если вы сообщили об уязвимости, которая была исправлена, мы добавим ваше имя в наш Зал славы безопасности.
28 |
29 | ## Обновления безопасности
30 |
31 | Обновления безопасности будут выпускаться в новых версиях проекта. Пожалуйста, убедитесь, что вы используете последнюю версию, чтобы воспользоваться последними исправлениями безопасности.
32 |
33 | ## Контакт
34 |
35 | По любым вопросам или проблемам, связанным с безопасностью, пожалуйста, свяжитесь с нами по адресу [work.w1tnessbtw@mail.ru](mailto:work.w1tnessbtw@mail.ru)
36 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/.env.example:
--------------------------------------------------------------------------------
1 | POSTGRES_HOST=str
2 | POSTGRES_PORT=int
3 | POSTGRES_USER=str
4 | POSTGRES_PASSWORD=str
5 | POSTGRES_DB=str
6 |
7 | API_BASE_PORT=int
8 |
9 | JWT_SECRET_KEY=str
10 | JWT_ACCESS_TOKEN_LIFETIME_MINUTES=int
11 | JWT_REFRESH_TOKEN_LIFETIME_HOURS=int
12 | JWT_ALGORITHM=str
13 |
14 | MAIL_EMAIL=str
15 | MAIL_PASSWORD=str
16 |
17 | TELEGRAM_API_TOKEN=str
18 | TELEGRAM_CHAT_ID=str
19 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/Makefile:
--------------------------------------------------------------------------------
1 | dev:
2 | poetry run uvicorn main:api --reload --port 8000
3 |
4 | rev:
5 | poetry run alembic revision --autogenerate
6 |
7 | mig:
8 | poetry run alembic upgrade head
9 |
10 | kill:
11 | taskkill /f /im python.exe
12 |
13 | migration:
14 | poetry run alembic revision --autogenerate
15 | poetry run alembic upgrade head
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | # Use forward slashes (/) also on windows to provide an os agnostic path
6 | script_location = alembic
7 |
8 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
9 | # Uncomment the line below if you want the files to be prepended with date and time
10 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
11 | # for all available tokens
12 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
13 |
14 | # sys.path path, will be prepended to sys.path if present.
15 | # defaults to the current working directory.
16 | prepend_sys_path = .
17 |
18 | # timezone to use when rendering the date within the migration file
19 | # as well as the filename.
20 | # If specified, requires the python>=3.9 or backports.zoneinfo library.
21 | # Any required deps can installed by adding `alembic[tz]` to the pip requirements
22 | # string value is passed to ZoneInfo()
23 | # leave blank for localtime
24 | # timezone =
25 |
26 | # max length of characters to apply to the "slug" field
27 | # truncate_slug_length = 40
28 |
29 | # set to 'true' to run the environment during
30 | # the 'revision' command, regardless of autogenerate
31 | # revision_environment = false
32 |
33 | # set to 'true' to allow .pyc and .pyo files without
34 | # a source .py file to be detected as revisions in the
35 | # versions/ directory
36 | # sourceless = false
37 |
38 | # version location specification; This defaults
39 | # to alembic/versions. When using multiple version
40 | # directories, initial revisions must be specified with --version-path.
41 | # The path separator used here should be the separator specified by "version_path_separator" below.
42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43 |
44 | # version path separator; As mentioned above, this is the character used to split
45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47 | # Valid values for version_path_separator are:
48 | #
49 | # version_path_separator = :
50 | # version_path_separator = ;
51 | # version_path_separator = space
52 | # version_path_separator = newline
53 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
54 |
55 | # set to 'true' to search source files recursively
56 | # in each "version_locations" directory
57 | # new in Alembic version 1.10
58 | # recursive_version_locations = false
59 |
60 | # the output encoding used when revision files
61 | # are written from script.py.mako
62 | # output_encoding = utf-8
63 |
64 | sqlalchemy.url = driver://user:pass@localhost/dbname
65 |
66 |
67 | [post_write_hooks]
68 | # post_write_hooks defines scripts or Python functions that are run
69 | # on newly generated revision scripts. See the documentation for further
70 | # detail and examples
71 |
72 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
73 | # hooks = black
74 | # black.type = console_scripts
75 | # black.entrypoint = black
76 | # black.options = -l 79 REVISION_SCRIPT_FILENAME
77 |
78 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
79 | # hooks = ruff
80 | # ruff.type = exec
81 | # ruff.executable = %(here)s/.venv/bin/ruff
82 | # ruff.options = --fix REVISION_SCRIPT_FILENAME
83 |
84 | # Logging configuration
85 | [loggers]
86 | keys = root,sqlalchemy,alembic
87 |
88 | [handlers]
89 | keys = console
90 |
91 | [formatters]
92 | keys = generic
93 |
94 | [logger_root]
95 | level = WARNING
96 | handlers = console
97 | qualname =
98 |
99 | [logger_sqlalchemy]
100 | level = WARNING
101 | handlers =
102 | qualname = sqlalchemy.engine
103 |
104 | [logger_alembic]
105 | level = INFO
106 | handlers =
107 | qualname = alembic
108 |
109 | [handler_console]
110 | class = StreamHandler
111 | args = (sys.stderr,)
112 | level = NOTSET
113 | formatter = generic
114 |
115 | [formatter_generic]
116 | format = %(levelname)-5.5s [%(name)s] %(message)s
117 | datefmt = %H:%M:%S
118 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/env.py:
--------------------------------------------------------------------------------
1 | from logging.config import fileConfig
2 |
3 | from sqlalchemy import engine_from_config
4 | from sqlalchemy import pool
5 |
6 | from alembic import context
7 |
8 | from app.cfg.settings import settings
9 |
10 | from app.database.models.models import Base
11 |
12 | config = context.config
13 | config.set_main_option("sqlalchemy.url", f"{settings.db_url}?async_fallback=True")
14 |
15 | if config.config_file_name is not None:
16 | fileConfig(config.config_file_name)
17 |
18 |
19 | target_metadata = Base.metadata
20 |
21 | def run_migrations_offline() -> None:
22 | """Run migrations in 'offline' mode.
23 |
24 | This configures the context with just a URL
25 | and not an Engine, though an Engine is acceptable
26 | here as well. By skipping the Engine creation
27 | we don't even need a DBAPI to be available.
28 |
29 | Calls to context.execute() here emit the given string to the
30 | script output.
31 |
32 | """
33 | url = config.get_main_option("sqlalchemy.url")
34 | context.configure(
35 | url=url,
36 | target_metadata=target_metadata,
37 | literal_binds=True,
38 | dialect_opts={"paramstyle": "named"},
39 | )
40 |
41 | with context.begin_transaction():
42 | context.run_migrations()
43 |
44 |
45 | def run_migrations_online() -> None:
46 | """Run migrations in 'online' mode.
47 |
48 | In this scenario we need to create an Engine
49 | and associate a connection with the context.
50 |
51 | """
52 | connectable = engine_from_config(
53 | config.get_section(config.config_ini_section, {}),
54 | prefix="sqlalchemy.",
55 | poolclass=pool.NullPool,
56 | )
57 |
58 | with connectable.connect() as connection:
59 | context.configure(connection=connection, target_metadata=target_metadata)
60 |
61 | with context.begin_transaction():
62 | context.run_migrations()
63 |
64 |
65 | if context.is_offline_mode():
66 | run_migrations_offline()
67 | else:
68 | run_migrations_online()
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from typing import Sequence, Union
9 |
10 | from alembic import op
11 | import sqlalchemy as sa
12 | ${imports if imports else ""}
13 |
14 | # revision identifiers, used by Alembic.
15 | revision: str = ${repr(up_revision)}
16 | down_revision: Union[str, None] = ${repr(down_revision)}
17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19 |
20 |
21 | def upgrade() -> None:
22 | ${upgrades if upgrades else "pass"}
23 |
24 |
25 | def downgrade() -> None:
26 | ${downgrades if downgrades else "pass"}
27 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/versions/09ac58fca3de_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 09ac58fca3de
4 | Revises: d911c2926905
5 | Create Date: 2025-04-19 19:16:11.073852
6 |
7 | """
8 | from typing import Sequence, Union
9 |
10 | from alembic import op
11 | import sqlalchemy as sa
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision: str = '09ac58fca3de'
16 | down_revision: Union[str, None] = 'd911c2926905'
17 | branch_labels: Union[str, Sequence[str], None] = None
18 | depends_on: Union[str, Sequence[str], None] = None
19 |
20 |
21 | def upgrade() -> None:
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | op.drop_constraint('team_projects_teamId_fkey', 'team_projects', type_='foreignkey')
24 | op.drop_constraint('team_projects_projectId_fkey', 'team_projects', type_='foreignkey')
25 | op.create_foreign_key(None, 'team_projects', 'teams', ['teamId'], ['id'], ondelete='CASCADE')
26 | op.create_foreign_key(None, 'team_projects', 'projects', ['projectId'], ['id'], ondelete='CASCADE')
27 | # ### end Alembic commands ###
28 |
29 |
30 | def downgrade() -> None:
31 | # ### commands auto generated by Alembic - please adjust! ###
32 | op.drop_constraint(None, 'team_projects', type_='foreignkey')
33 | op.drop_constraint(None, 'team_projects', type_='foreignkey')
34 | op.create_foreign_key('team_projects_projectId_fkey', 'team_projects', 'projects', ['projectId'], ['id'])
35 | op.create_foreign_key('team_projects_teamId_fkey', 'team_projects', 'teams', ['teamId'], ['id'])
36 | # ### end Alembic commands ###
37 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/versions/29f26748a5e4_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 29f26748a5e4
4 | Revises:
5 | Create Date: 2025-04-12 14:51:34.389625
6 |
7 | """
8 | from typing import Sequence, Union
9 |
10 | from alembic import op
11 | import sqlalchemy as sa
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision: str = '29f26748a5e4'
16 | down_revision: Union[str, None] = None
17 | branch_labels: Union[str, Sequence[str], None] = None
18 | depends_on: Union[str, Sequence[str], None] = None
19 |
20 |
21 | def upgrade() -> None:
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | pass
24 | # ### end Alembic commands ###
25 |
26 |
27 | def downgrade() -> None:
28 | # ### commands auto generated by Alembic - please adjust! ###
29 | pass
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/versions/36551275d3fe_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 36551275d3fe
4 | Revises: e238014fade3
5 | Create Date: 2025-04-19 18:37:47.025347
6 |
7 | """
8 | from typing import Sequence, Union
9 |
10 | from alembic import op
11 | import sqlalchemy as sa
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision: str = '36551275d3fe'
16 | down_revision: Union[str, None] = 'e238014fade3'
17 | branch_labels: Union[str, Sequence[str], None] = None
18 | depends_on: Union[str, Sequence[str], None] = None
19 |
20 |
21 | def upgrade() -> None:
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | op.add_column('projects', sa.Column('creator_id', sa.UUID(), nullable=True))
24 | op.create_foreign_key(None, 'projects', 'users', ['creator_id'], ['id'], ondelete='CASCADE')
25 | # ### end Alembic commands ###
26 |
27 |
28 | def downgrade() -> None:
29 | # ### commands auto generated by Alembic - please adjust! ###
30 | op.drop_constraint(None, 'projects', type_='foreignkey')
31 | op.drop_column('projects', 'creator_id')
32 | # ### end Alembic commands ###
33 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/versions/d911c2926905_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: d911c2926905
4 | Revises: 36551275d3fe
5 | Create Date: 2025-04-19 19:06:34.955357
6 |
7 | """
8 | from typing import Sequence, Union
9 |
10 | from alembic import op
11 | import sqlalchemy as sa
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision: str = 'd911c2926905'
16 | down_revision: Union[str, None] = '36551275d3fe'
17 | branch_labels: Union[str, Sequence[str], None] = None
18 | depends_on: Union[str, Sequence[str], None] = None
19 |
20 |
21 | def upgrade() -> None:
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | pass
24 | # ### end Alembic commands ###
25 |
26 |
27 | def downgrade() -> None:
28 | # ### commands auto generated by Alembic - please adjust! ###
29 | pass
30 | # ### end Alembic commands ###
31 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/alembic/versions/e238014fade3_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: e238014fade3
4 | Revises: 29f26748a5e4
5 | Create Date: 2025-04-19 17:36:01.775078
6 |
7 | """
8 | from typing import Sequence, Union
9 |
10 | from alembic import op
11 | import sqlalchemy as sa
12 |
13 |
14 | # revision identifiers, used by Alembic.
15 | revision: str = 'e238014fade3'
16 | down_revision: Union[str, None] = '29f26748a5e4'
17 | branch_labels: Union[str, Sequence[str], None] = None
18 | depends_on: Union[str, Sequence[str], None] = None
19 |
20 |
21 | def upgrade() -> None:
22 | # ### commands auto generated by Alembic - please adjust! ###
23 | op.drop_constraint('team_members_userId_fkey', 'team_members', type_='foreignkey')
24 | op.create_foreign_key(None, 'team_members', 'users', ['userId'], ['id'], ondelete='CASCADE')
25 | op.drop_constraint('team_moderators_userId_fkey', 'team_moderators', type_='foreignkey')
26 | op.create_foreign_key(None, 'team_moderators', 'users', ['userId'], ['id'], ondelete='CASCADE')
27 | # ### end Alembic commands ###
28 |
29 |
30 | def downgrade() -> None:
31 | # ### commands auto generated by Alembic - please adjust! ###
32 | op.drop_constraint(None, 'team_moderators', type_='foreignkey')
33 | op.create_foreign_key('team_moderators_userId_fkey', 'team_moderators', 'users', ['userId'], ['id'])
34 | op.drop_constraint(None, 'team_members', type_='foreignkey')
35 | op.create_foreign_key('team_members_userId_fkey', 'team_members', 'users', ['userId'], ['id'])
36 | # ### end Alembic commands ###
37 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/cfg/settings.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings
2 | from yarl import URL
3 | class Settings(BaseSettings):
4 | POSTGRES_HOST: str
5 | POSTGRES_PORT: int
6 | POSTGRES_USER: str
7 | POSTGRES_PASSWORD: str
8 | POSTGRES_DB: str
9 |
10 | API_BASE_PORT: int
11 |
12 | JWT_SECRET_KEY: str
13 | JWT_ACCESS_TOKEN_LIFETIME_MINUTES: int
14 | JWT_REFRESH_TOKEN_LIFETIME_HOURS: int
15 | JWT_ALGORITHM: str
16 |
17 | MAIL_EMAIL: str
18 | MAIL_PASSWORD: str
19 |
20 | TELEGRAM_API_TOKEN: str
21 | TELEGRAM_CHAT_ID: str
22 |
23 | class Config:
24 | env_file = ".env"
25 |
26 | @property
27 | def db_url(self) -> URL:
28 | url = URL.build (
29 | scheme="postgresql+asyncpg",
30 | host=self.POSTGRES_HOST,
31 | port=self.POSTGRES_PORT,
32 | user=self.POSTGRES_USER,
33 | password=self.POSTGRES_PASSWORD,
34 | path=f"/{self.POSTGRES_DB}"
35 | )
36 | return url
37 |
38 | settings: Settings = Settings()
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/abstract/abc_repo.py:
--------------------------------------------------------------------------------
1 | from abc import ABC
2 |
3 | from sqlalchemy import select, delete, update, insert
4 | from sqlalchemy.ext.asyncio import AsyncSession
5 | from sqlalchemy.exc import SQLAlchemyError
6 | class AbstractRepository(ABC):
7 |
8 | def __init__(self, session: AsyncSession):
9 | self._session = session
10 |
11 | model = None
12 |
13 | async def commit(self):
14 | try:
15 | await self._session.commit()
16 | except SQLAlchemyError as e:
17 | await self._session.rollback()
18 | raise e
19 |
20 | async def rollback(self):
21 | await self._session.rollback()
22 |
23 | async def get_by_id(self, id):
24 | return await self._session.get(self.model, id)
25 |
26 | async def get_all(self):
27 | result = await self._session.execute(select(self.model))
28 | return result.scalars().all()
29 |
30 | async def create(self, **kwargs):
31 | query = insert(self.model).values(**kwargs).returning(self.model)
32 | result = await self._session.execute(query)
33 | await self.commit()
34 | return result.scalars().first()
35 |
36 | async def delete_by_id(self, id):
37 | result = await self._session.execute(delete(self.model).where(self.model.id == id))
38 | return result.rowcount
39 |
40 | async def get_by_filter_all(self, **kwargs):
41 | query = select(self.model).filter_by(**kwargs)
42 | result = await self._session.execute(query)
43 | return result.scalars().all()
44 |
45 | async def update_by_id(self, id, **kwargs):
46 | query = update(self.model).where(self.model.id == id).values(**kwargs).returning(self.model)
47 | result = await self._session.execute(query)
48 | await self.commit()
49 | return result.scalars().first()
50 |
51 | async def get_by_filter_one(self, **kwargs):
52 | query = select(self.model).filter_by(**kwargs)
53 | result = await self._session.execute(query)
54 | return result.scalars().one_or_none()
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/database.py:
--------------------------------------------------------------------------------
1 | from contextlib import asynccontextmanager
2 | from sqlalchemy.ext.asyncio import (
3 | AsyncSession,
4 | create_async_engine,
5 | AsyncEngine
6 | )
7 |
8 | from sqlalchemy.orm import sessionmaker
9 |
10 |
11 | from app.cfg.settings import settings
12 | from app.utils.logger.telegram_bot.telegram_logger import logger
13 | from app.database.models.models import Base
14 |
15 | async def get_engine() -> AsyncEngine:
16 | return create_async_engine(str(settings.db_url))
17 |
18 |
19 |
20 | async def get_session():
21 | async_session = sessionmaker(await get_engine(), class_=AsyncSession, expire_on_commit=False)
22 | async with async_session() as session:
23 | try:
24 | yield session
25 | finally:
26 | await session.close()
27 |
28 | async def create_tables():
29 | try:
30 | engine = await get_engine()
31 | async with engine.begin() as eng:
32 | await eng.run_sync(Base.metadata.create_all)
33 | print("migration successfully")
34 | except Exception as e:
35 | logger.fatal("Не удалось создать таблицы. ", str(e))
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/models/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from sqlalchemy import Date, ForeignKey
4 | from sqlalchemy.orm import Mapped, mapped_column, relationship, declarative_base
5 | from sqlalchemy.dialects.postgresql import UUID
6 |
7 | import uuid
8 |
9 | from typing import List
10 |
11 | from app.utils.verify_codes_generator.code_generator import generate_confirmation_code
12 |
13 | Base = declarative_base()
14 |
15 | class Objective(Base):
16 | __tablename__ = "objectives"
17 |
18 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
19 | title: Mapped[str] = mapped_column()
20 | description: Mapped[str] = mapped_column(nullable=True)
21 | parent_objectiveId: Mapped[UUID] = mapped_column(ForeignKey("objectives.id"), nullable=True)
22 | projectId: Mapped[UUID] = mapped_column(UUID, ForeignKey("projects.id", ondelete='CASCADE'), nullable=False)
23 |
24 | created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.now())
25 | deadline_date: Mapped[datetime.datetime] = mapped_column(
26 | default=datetime.datetime.now() + datetime.timedelta(days=7),
27 | )
28 |
29 | author_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
30 | author: Mapped["User"] = relationship("User", back_populates="authored_objectives")
31 |
32 | project: Mapped["Project"] = relationship("Project", back_populates="objectives")
33 | parent_objective: Mapped["Objective"] = relationship("Objective", back_populates="child_objectives", remote_side=[id])
34 | child_objectives: Mapped[list["Objective"]] = relationship("Objective", back_populates="parent_objective")
35 |
36 |
37 |
38 | class Organization(Base):
39 | __tablename__ = "organizations"
40 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
41 | title: Mapped[str] = mapped_column()
42 | description: Mapped[str] = mapped_column(nullable=True)
43 | leaderId: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
44 | leader: Mapped["User"] = relationship("User", back_populates="organizations")
45 | teams: Mapped[List["Team"]] = relationship("Team", back_populates="organization") # type: ignore
46 |
47 | class Project(Base):
48 | __tablename__ = "projects"
49 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
50 | title: Mapped[str] = mapped_column()
51 | description: Mapped[str] = mapped_column(nullable=True)
52 | creator_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
53 | created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.now())
54 |
55 |
56 | creator: Mapped["User"] = relationship("User", back_populates="created_projects")
57 | objectives: Mapped[List["Objective"]] = relationship("Objective", back_populates="project")
58 | moderators: Mapped["ProjectModerator"] = relationship("ProjectModerator", back_populates="project")
59 | members: Mapped[List["ProjectMember"]] = relationship("ProjectMember", back_populates="project")
60 |
61 | class TeamModerator(Base):
62 | __tablename__ = "team_moderators"
63 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
64 | userId: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
65 | teamId: Mapped[UUID] = mapped_column(ForeignKey("teams.id"))
66 |
67 | team: Mapped["Team"] = relationship("Team", back_populates="moderators") # type: ignore
68 | user: Mapped["User"] = relationship("User", back_populates="teams_moderation") # type: ignore
69 |
70 | class TeamMember(Base):
71 | __tablename__ = "team_members"
72 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
73 | userId: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
74 | teamId: Mapped[UUID] = mapped_column(ForeignKey("teams.id"))
75 |
76 | team: Mapped["Team"] = relationship("Team", back_populates="members") # type: ignore
77 | user: Mapped["User"] = relationship("User", back_populates="teams") # type: ignore
78 |
79 |
80 | class TeamProject(Base):
81 | __tablename__ = "team_projects"
82 |
83 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
84 | teamId: Mapped[UUID] = mapped_column(ForeignKey("teams.id", ondelete="CASCADE"))
85 | projectId: Mapped[UUID] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
86 | team: Mapped["Team"] = relationship("Team", back_populates="projects") # type: ignore
87 | project: Mapped["Project"] = relationship("Project") # type: ignore
88 |
89 | class ProjectModerator(Base):
90 | __tablename__ = "project_moderators"
91 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
92 | userId: Mapped[UUID] = mapped_column(ForeignKey("users.id"))
93 | projectId: Mapped[UUID] = mapped_column(ForeignKey("projects.id"))
94 |
95 | project: Mapped["Project"] = relationship("Project", back_populates="moderators") # type: ignore
96 | user: Mapped["User"] = relationship("User", back_populates="projects_moderation") # type: ignore
97 |
98 | class Team(Base):
99 | __tablename__ = "teams"
100 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
101 | title: Mapped[str] = mapped_column()
102 | description: Mapped[str] = mapped_column(nullable=True)
103 | leaderId: Mapped[UUID] = mapped_column(ForeignKey("users.id"), nullable=True)
104 | organizationId: Mapped[UUID] = mapped_column(ForeignKey("organizations.id", ondelete="SET NULL"), nullable=True)
105 |
106 | organization: Mapped["Organization"] = relationship("Organization", back_populates="teams") # type: ignore
107 | leader: Mapped["User"] = relationship("User", back_populates="teams_lead")
108 | members: Mapped[list["TeamMember"]] = relationship("TeamMember", back_populates="team") # type: ignore
109 | projects: Mapped[list["TeamProject"]] = relationship("TeamProject", back_populates="team") # type: ignore
110 | moderators: Mapped[list["TeamModerator"]] = relationship("TeamModerator", back_populates="team") # type: ignore
111 |
112 | class UserProfile(Base):
113 | __tablename__ = "user_profiles"
114 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
115 | surname: Mapped[str] = mapped_column(nullable=True)
116 | name: Mapped[str] = mapped_column(nullable=True)
117 | patronymic: Mapped[str] = mapped_column(nullable=True)
118 | birthdate: Mapped[Date] = mapped_column(Date, nullable=True, default=datetime.date.today())
119 | about: Mapped[str] = mapped_column(nullable=True)
120 | userId: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete='CASCADE'))
121 |
122 | user: Mapped["User"] = relationship("User", back_populates="profile") # type: ignore
123 |
124 | class EmailVerification(Base):
125 | __tablename__ = "email_verifications"
126 |
127 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
128 | userId: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete='CASCADE'), nullable=False)
129 | verification_code: Mapped[str] = mapped_column(default=str(generate_confirmation_code()))
130 | created_at: Mapped[datetime.datetime] = mapped_column(default=datetime.datetime.utcnow())
131 |
132 | user: Mapped["User"] = relationship("User", back_populates="verification")
133 |
134 |
135 | class User(Base):
136 | __tablename__ = "users"
137 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
138 | email: Mapped[str] = mapped_column(nullable=True, unique=True)
139 | password: Mapped[str] = mapped_column()
140 | registered: Mapped[Date] = mapped_column(Date, default=datetime.date.today(), nullable=True)
141 | is_mail_verified: Mapped[bool] = mapped_column(default=False, nullable=False)
142 |
143 |
144 | team_invitations: Mapped[List["TeamInvitation"]] = relationship("TeamInvitation", back_populates="invited_user", foreign_keys="[TeamInvitation.invited_user_id]", cascade="all, delete-orphan")
145 | project_invitations: Mapped[List["ProjectInvitation"]] = relationship("ProjectInvitation", back_populates="invited_user", foreign_keys="[ProjectInvitation.invited_user_id]", cascade="all, delete-orphan")
146 | profile: Mapped["UserProfile"] = relationship("UserProfile", back_populates="user") # type: ignore
147 | teams: Mapped[List["TeamMember"]] = relationship("TeamMember", back_populates="user", cascade="all, delete-orphan") # type: ignore
148 | projects: Mapped[List["ProjectMember"]] = relationship("ProjectMember", back_populates="user", cascade="all, delete-orphan")
149 | teams_lead: Mapped[List["Team"]] = relationship("Team", back_populates="leader")
150 | organizations: Mapped[List["Organization"]] = relationship("Organization", back_populates="leader", cascade="all, delete-orphan")
151 | verification: Mapped["EmailVerification"] = relationship("EmailVerification", back_populates="user")
152 | teams_moderation: Mapped[List["TeamModerator"]] = relationship("TeamModerator", back_populates="user")
153 | projects_moderation: Mapped[List["ProjectModerator"]] = relationship("ProjectModerator", back_populates="user")
154 | created_projects: Mapped[List["Project"]] = relationship("Project", back_populates="creator")
155 | authored_objectives: Mapped[List["Objective"]] = relationship("Objective", back_populates="author")
156 |
157 | class TeamInvitation(Base):
158 | __tablename__ = "team_invitations"
159 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
160 | invited_user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"))
161 | invited_by_leader_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"))
162 | teamId: Mapped[UUID] = mapped_column(ForeignKey("teams.id"))
163 |
164 | invited_user: Mapped["User"] = relationship("User", back_populates="team_invitations", foreign_keys="[TeamInvitation.invited_user_id]")
165 | invited_by_leader: Mapped["User"] = relationship("User", foreign_keys="[TeamInvitation.invited_by_leader_id]")
166 | team: Mapped["Team"] = relationship("Team")
167 |
168 | class ProjectMember(Base):
169 | __tablename__ = "project_members"
170 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
171 | userId: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete='CASCADE'))
172 | projectId: Mapped[UUID] = mapped_column(ForeignKey("projects.id", ondelete='CASCADE'))
173 |
174 | user: Mapped["User"] = relationship("User", back_populates="projects")
175 | project: Mapped["Project"] = relationship("Project", back_populates="members")
176 |
177 | class ProjectInvitation(Base):
178 | __tablename__ = "project_invitations"
179 | id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
180 | invited_user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete='CASCADE'))
181 | invited_by_leader_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete='CASCADE'))
182 | projectId: Mapped[UUID] = mapped_column(ForeignKey("projects.id", ondelete='CASCADE'))
183 |
184 | invited_user: Mapped["User"] = relationship("User", back_populates="project_invitations", foreign_keys="[ProjectInvitation.invited_user_id]")
185 | invited_by_leader: Mapped["User"] = relationship("User", foreign_keys="[ProjectInvitation.invited_by_leader_id]")
186 | project: Mapped["Project"] = relationship("Project")
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/organization_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import Organization
3 |
4 | class OrganizationRepository(AbstractRepository):
5 | model = Organization
6 |
7 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/project_invitation_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import ProjectInvitation
3 |
4 | class ProjectInvitationRepository(AbstractRepository):
5 | model = ProjectInvitation
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/project_members_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import ProjectMember
3 |
4 | class ProjectMembersRepository(AbstractRepository):
5 | model = ProjectMember
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/project_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import Project
3 |
4 | class ProjectRepository(AbstractRepository):
5 | model = Project
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/team_invitation_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import TeamInvitation
3 |
4 | class TeamInvitationRepository(AbstractRepository):
5 | model = TeamInvitation
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/team_member_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import TeamMember
3 |
4 | class TeamMemberRepository(AbstractRepository):
5 | model = TeamMember
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/team_project_repository.py:
--------------------------------------------------------------------------------
1 | from app.database.abstract.abc_repo import AbstractRepository
2 | from app.database.models.models import TeamProject
3 |
4 | class TeamProjectRepository(AbstractRepository):
5 | model = TeamProject
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/team_repository.py:
--------------------------------------------------------------------------------
1 |
2 | from app.database.abstract.abc_repo import AbstractRepository
3 | from app.database.models.models import Team
4 |
5 |
6 | class TeamRepository(AbstractRepository):
7 | model = Team
8 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/user_profile_repository.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.asyncio import AsyncSession
2 |
3 | from app.security.hasher import verify_password
4 | from app.utils.result import *
5 | from ..abstract.abc_repo import AbstractRepository
6 | from app.database.models.models import UserProfile
7 | from datetime import datetime
8 | from sqlalchemy import CursorResult, delete, select, update, insert
9 | from typing import Optional
10 |
11 | class UserProfileRepository(AbstractRepository):
12 | model = UserProfile
13 |
14 | async def update(self, userId: str, **kwargs):
15 | query = update(self.model).where(self.model.userId == userId).values(**kwargs).returning(self.model)
16 | result = await self._session.execute(query)
17 | await self._session.commit()
18 | return result.scalars().first()
19 |
20 | async def update_profile(self,
21 | userId: str,
22 | surname: Optional[str],
23 | name: Optional[str],
24 | patronymic: Optional[str],
25 | birthdate: Optional[datetime]
26 | ) -> Result[None]:
27 |
28 | query = (
29 | select(self.model)
30 | .where(self.model.userId == userId)
31 | )
32 |
33 | result = await self._session.execute(query)
34 | profile = result.scalars().one_or_none()
35 | if not profile:
36 | creating = await self.create(userId=userId, surname=surname, name=name, patronymic=patronymic, birthdate=birthdate)
37 | return success(creating) if creating else err("Some error while attemping resource.")
38 | return success(await self.update(userId=userId, surname=surname, name=name, patronymic=patronymic, birthdate=birthdate))
39 |
40 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/user_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from sqlalchemy.ext.asyncio import AsyncSession
3 |
4 | from app.security.hasher import verify_password
5 |
6 | from app.utils.result import Result, err, success
7 | from app.utils.logger.telegram_bot.telegram_logger import logger
8 |
9 | from app.database.models.models import User, TeamModerator
10 | from app.database.abstract.abc_repo import AbstractRepository
11 |
12 | from sqlalchemy import delete, select, update, insert
13 |
14 | class UserRepository(AbstractRepository):
15 | model = User
16 |
17 | async def get_moderation_teams(self, userId: str):
18 | query = (
19 | select(self.model)
20 | .where(self.model.id == userId)
21 | .join(TeamModerator, TeamModerator.id == self.model.id)
22 | )
23 |
24 | result = await self._session.execute(query)
25 | user = result.mappings().all()
26 | return user
27 |
28 | async def create(self, **kwargs):
29 | query = insert(self.model).values(**kwargs).returning(self.model)
30 | result = await self._session.execute(query)
31 | return result.scalars().first()
32 |
33 | async def authenticate_user(self, email: str, password: str) -> Result:
34 | user = await UserRepository(self._session).get_by_filter_one(email=email)
35 | if not user:
36 | return err("User not found")
37 | if not verify_password(password, user.password):
38 | return err("Invalid password")
39 | return success(user)
40 |
41 | async def get_by_email(self, email) -> Optional[User]:
42 | try:
43 |
44 | result = await self._session.execute(select(self.model).where(self.model.email == email))
45 | user = result.scalars().first()
46 | if not user:
47 | return None
48 | return user
49 |
50 | except Exception as e:
51 | logger.error(f"Произошла ошибка в UserRepository.", str(e))
52 |
53 | async def delete_by_id(self, id) -> Result[int]:
54 | try:
55 | result = await self._session.execute(delete(self.model).where(self.model.userId == id))
56 | await self._session.commit()
57 | return success(result.rowcount)
58 | except Exception as e:
59 | return err(str(e))
60 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/database/repo/verification_repository.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import Annotated, Callable, Optional
3 | from app.database.abstract.abc_repo import AbstractRepository
4 | from app.database.models.models import EmailVerification, User
5 |
6 | from app.utils.email_service.email_service import EmailService
7 | from app.utils.result import Result, err, success
8 | from app.utils.verify_codes_generator.code_generator import generate_confirmation_code
9 | from app.utils.logger.telegram_bot.telegram_logger import logger
10 |
11 | from sqlalchemy import and_, desc, select, update, delete, insert
12 | from sqlalchemy.exc import IntegrityError
13 |
14 |
15 | class VerificationRepository(AbstractRepository):
16 | model = EmailVerification
17 |
18 | async def resend_verification(self, userId: str, verification_code: str) -> Result[Optional[EmailVerification]]:
19 |
20 | clear_query = (
21 | delete(self.model)
22 | .where(self.model.id == userId)
23 | )
24 |
25 | await self._session.execute(clear_query)
26 | await self._session.commit()
27 |
28 | try:
29 | return success(await self.create(id=userId, verification_code=verification_code))
30 | except Exception as e:
31 | logger.error("Произшла ошибка в репозитории верификации.")
32 | return err("Произошла ошибка. Информация уже направлена разработчику")
33 |
34 | async def process_verification(self, userId: str, verification_code: str) -> Result[None]:
35 | last_verification_query = (
36 | select(self.model)
37 | .order_by(desc(self.model.created_at))
38 | .where(self.model.userId == userId)
39 | )
40 |
41 | result = await self._session.execute(last_verification_query)
42 | last_verification = result.scalars().first()
43 | if not last_verification:
44 | verification = await self.create(userId=userId, verification_code=verification_code)
45 | return success(verification)
46 |
47 | return err("Верификация уже была пройдена.")
48 |
49 |
50 | async def confirm_email(self, userId: str, code: str) -> Result[None]:
51 | logger.info(str(datetime.datetime.now(datetime.timezone.utc)))
52 | try:
53 | query = (
54 | select(self.model)
55 | .where(
56 | self.model.userId == userId,
57 | )
58 | .order_by(desc(self.model.created_at))
59 | )
60 |
61 | result = await self._session.execute(query)
62 | verification = result.scalars().first()
63 |
64 | if datetime.datetime.now() > verification.created_at + datetime.timedelta(minutes=15):
65 | return err("Время верификации истекло. Запросите верификацию повторно.")
66 |
67 | if code == verification.verification_code:
68 | await self.delete_by_id(userId)
69 | return success("Верификация прошла успешно.")
70 | return err("Код не верный.")
71 | except Exception as e:
72 | logger.error(e)
73 | return err("Проиизошла ошибка в сервисе верификации. Информация направлена разработчику.")
74 |
75 | async def delete_by_id(self, id) -> Result[int]:
76 | try:
77 | result = await self._session.execute(
78 | delete(self.model).where(self.model.id == id)
79 | )
80 | await self._session.commit()
81 | return success(result.rowcount)
82 | except:
83 | return err(0)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/exc/bad_email.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class BadEmail(Exception):
4 | def __init__(self, message="Email указан в неверном формате."):
5 | self.message = message
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/access_router.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta, timezone
2 | from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
3 | from fastapi.responses import JSONResponse
4 | from fastapi.security import OAuth2PasswordRequestForm
5 |
6 | from app.database.models.models import User
7 | from app.database.repo.user_repository import UserRepository
8 | from app.database.database import get_session
9 |
10 | from app.schema.response.AccessToken import AccessToken
11 | from app.schema.request.get_access.register import RegisterRequest
12 |
13 | from app.services.user_service import UserService
14 |
15 | from app.services.verification_service import VerificationService
16 |
17 | from app.utils.result import Result
18 | from app.utils.verify_codes_generator.code_generator import generate_confirmation_code
19 |
20 | from app.security.jwttype import JWTType
21 | from app.security.jwtmanager import JWTManager, get_current_user, oauth2_scheme
22 | from app.security.jwtmanager import oauth2_scheme
23 |
24 | from app.exc.bad_email import BadEmail
25 |
26 | from sqlalchemy.ext.asyncio import AsyncSession
27 |
28 | from app.cfg.settings import settings
29 |
30 | auth_router = APIRouter(prefix="/access", tags=["System Access"])
31 |
32 |
33 |
34 | @auth_router.post("/register")
35 | async def register(registerRequest: RegisterRequest, session: AsyncSession = Depends(get_session)):
36 | registered = await UserService(session).register(registerRequest)
37 | if not registered.success:
38 | raise HTTPException(status_code=400, detail=registered.error)
39 | return {
40 | "message": registered.value
41 | }
42 |
43 |
44 | @auth_router.post("/authorize")
45 | async def authorize(
46 | response: Response,
47 | username: str = Form(),
48 | password: str = Form(),
49 | session: AsyncSession = Depends(get_session)):
50 |
51 | user = await UserRepository(session).get_by_filter_one(email=username)
52 | if not user:
53 | raise HTTPException(status_code=401, detail="Пользователь не найден")
54 | authorized = await UserService(session).authorize(username, password)
55 | if not authorized.success:
56 | raise HTTPException(status_code=401, detail=authorized.error)
57 |
58 | jwt_manager = JWTManager()
59 | access_token = jwt_manager.encode_token({ "userId": str(user.id) }, token_type=JWTType.ACCESS)
60 | refresh_token = jwt_manager.encode_token({ "userId": str(user.id) }, token_type=JWTType.REFRESH)
61 |
62 | access_token_expires = (datetime.now(tz=timezone.utc) + timedelta(minutes=settings.JWT_ACCESS_TOKEN_LIFETIME_MINUTES)).strftime("%a, %d-%b-%Y %H:%M:%S GMT")
63 | refresh_token_expires = (datetime.now(tz=timezone.utc) + timedelta(days=settings.JWT_REFRESH_TOKEN_LIFETIME_HOURS)).strftime("%a, %d-%b-%Y %H:%M:%S GMT")
64 |
65 | access_token_cookie = (
66 | f"access_token={access_token};"
67 | f" Expires={access_token_expires};"
68 | " HttpOnly; Path=/; Secure; SameSite=None; Partitioned"
69 | )
70 |
71 | refresh_token_cookie = (
72 | f"refresh_token={refresh_token};"
73 | f" Expires={refresh_token_expires};"
74 | " HttpOnly; Path=/; Secure; SameSite=None; Partitioned"
75 | )
76 |
77 | response.headers.append("Set-Cookie", access_token_cookie)
78 | response.headers.append("Set-Cookie", refresh_token_cookie)
79 |
80 | return AccessToken(
81 | access_token=access_token,
82 | refresh_token=refresh_token,
83 | token_type="Bearer"
84 | )
85 |
86 |
87 | async def refresh_access_token(request: Request):
88 |
89 | cookies = request.cookies
90 | refresh_token = cookies.get("refresh_token")
91 | if not refresh_token:
92 | raise HTTPException(status_code=400, detail="Refresh token is not provided")
93 |
94 | jwt_manager = JWTManager()
95 | token_data = jwt_manager.decode_token(refresh_token)
96 | if token_data.error:
97 | raise HTTPException(status_code=400, detail=token_data.error)
98 |
99 |
100 | session: AsyncSession = await get_session()
101 | user = await UserRepository(session).get_by_filter_one(userId=token_data.value["userId"])
102 | session.close()
103 | if not user:
104 | raise HTTPException(status_code=400, detail="User not found")
105 |
106 | return jwt_manager.encode_token({ "userId": str(user.userId), "email": user.email }, token_type=JWTType.ACCESS)
107 |
108 | @auth_router.get("/refresh")
109 | async def refresh(token: str = Depends(refresh_access_token)):
110 | response = JSONResponse(content = {
111 | "access_token": token
112 | })
113 | response.set_cookie(key="access_token", value=token, httponly=True)
114 | return response
115 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/account_router.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends, HTTPException
2 | from sqlalchemy.ext.asyncio import AsyncSession
3 |
4 | from app.database.models.models import User
5 | from app.security.jwtmanager import get_current_user, oauth2_scheme
6 |
7 | from app.services.organization_service import OrganizationService
8 | from app.services.team_service import TeamService
9 | from app.services.user_profile_service import UserProfileService
10 | from app.services.user_service import UserService
11 |
12 | from app.database.database import get_session
13 |
14 | from app.schema.request.account.updateprofile import UpdateProfile
15 |
16 | account_router = APIRouter(
17 | prefix="/account",
18 | tags=["Account"]
19 | )
20 |
21 | # @account_router.get("/my_organizations/get_all")
22 | # async def get_my_organizations(user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
23 | # return await OrganizationService(session).get_all_user_organizations(user.id)
24 |
25 | @account_router.get("/teams/get_all")
26 | async def get_my_teams(user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
27 | return await UserService(session).get_all_teams(user.id)
28 |
29 | @account_router.put("/updateprofile")
30 | async def update_profile(updateRequest: UpdateProfile, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
31 | result = await UserProfileService(session).update_profile(user.userId, updateRequest)
32 |
33 | if not result.success:
34 | raise HTTPException(
35 | status_code=400,
36 | detail=result.error
37 | )
38 |
39 | return result.value
40 |
41 | @account_router.delete("/delete")
42 | async def delete_account(user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
43 | result = await UserService(session).delete_user(user.userId)
44 |
45 | if not result.success:
46 | raise HTTPException(
47 | status_code=400,
48 | detail=result.error
49 | )
50 |
51 | return result.value
52 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/main_router.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from app.routing.project_router import project_router
3 | from app.routing.team.team_router import team_router
4 | from app.routing.organization_router import organization_router
5 | from app.routing.access_router import auth_router
6 | from app.routing.account_router import account_router
7 | from app.routing.verification_router import verification_router
8 |
9 | main_router = APIRouter (
10 | prefix = "/v1"
11 | )
12 |
13 | # cycling
14 | main_router.include_router(auth_router)
15 | main_router.include_router(verification_router)
16 | main_router.include_router(account_router)
17 |
18 | main_router.include_router(organization_router)
19 | main_router.include_router(team_router)
20 | main_router.include_router(project_router)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/organization_router.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends, HTTPException
2 | from sqlalchemy.ext.asyncio import AsyncSession
3 |
4 | from app.database.database import get_session
5 | from app.database.models.models import User
6 | from app.schema.request.organization.create_organization import CreateOrganization
7 | from app.security.jwtmanager import get_current_user
8 | from app.services.organization_service import OrganizationService
9 |
10 | organization_router = APIRouter(
11 | prefix="/organizations",
12 | tags=["Организации"]
13 | )
14 |
15 | @organization_router.post("/create")
16 | async def create_organization(organization: CreateOrganization, session: AsyncSession = Depends(get_session), user: User = Depends(get_current_user)):
17 | return await OrganizationService(session).create_organization(user.id, organization)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/project_router.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 | from fastapi import APIRouter, Depends
3 |
4 | from app.database.database import get_session
5 | from app.database.models.models import User
6 | from app.schema.request.project.create_project import CreateProject
7 | from app.security.jwtmanager import get_current_user
8 | from app.services.project_service import ProjectService
9 | from sqlalchemy.ext.asyncio import AsyncSession
10 | project_router = APIRouter(
11 | prefix="/projects",
12 | tags=["Проекты"]
13 | )
14 |
15 |
16 | @project_router.post("/create/{team_id}")
17 | async def create_project(team_id: UUID, project_data: CreateProject, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
18 | return await ProjectService(session).create_project(user.id, team_id, project_data)
19 |
20 | @project_router.post("/invites/accept/{invite_request_id}")
21 | async def join_project(invite_request_id: UUID, project_id: UUID, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
22 | return await ProjectService(session).approve_invite(user.id, project_id)
23 |
24 | @project_router.delete("/invites/decline/{invite_request_id}")
25 | async def leave_project(invite_request_id: UUID, project_id: UUID, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
26 | pass
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/team/team_invites_router.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 | from fastapi import APIRouter, Depends
3 |
4 | from app.database.database import get_session
5 | from app.database.models.models import User
6 | from app.security.jwtmanager import get_current_user
7 |
8 | from sqlalchemy.ext.asyncio import AsyncSession
9 |
10 | from app.services.team_service import TeamService
11 | team_invites_router = APIRouter(
12 | prefix="/invites",
13 | tags=["Team Invites"]
14 | )
15 |
16 |
17 | @team_invites_router.post("/invite")
18 | async def invite_to_team(team_id: UUID, user_id: UUID, moderator: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
19 | return await TeamService(session).invite_user(team_id, user_id, moderator.id)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/team/team_router.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 | from fastapi import APIRouter, HTTPException, Depends
3 |
4 | from sqlalchemy.ext.asyncio import AsyncSession
5 |
6 | from app.database.database import get_session
7 | from app.database.models.models import User
8 | from app.schema.request.team.create_team import CreateTeam
9 | from app.schema.request.team.update_team import UpdateTeam
10 | from app.security.jwtmanager import get_current_user
11 | from app.services.team_service import TeamService
12 |
13 | team_router = APIRouter(
14 | prefix="/teams",
15 | tags=["Team"]
16 | )
17 |
18 | # @team_router.get("/get_all")
19 | # async def get_all_teams(user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
20 | # return await TeamService(session).get_all_teams(user.id)
21 |
22 | @team_router.post("/create")
23 | async def create_team(team: CreateTeam, session: AsyncSession = Depends(get_session), user: User = Depends(get_current_user)):
24 | return await TeamService(session).create_team(team, user.id)
25 |
26 | @team_router.patch("/edit/{team_id}")
27 | async def edit_team(team_id: UUID, updates: UpdateTeam, session: AsyncSession = Depends(get_session), user: User = Depends(get_current_user)):
28 | if not await TeamService(session).is_user_team_moderator(user.id, team_id):
29 | raise HTTPException(
30 | status_code=403,
31 | detail="У вас нет прав на редактирование данной команды."
32 | )
33 | return await TeamService(session).update_team(team_id, updates)
34 |
35 | @team_router.delete("/delete/{team_id}")
36 | async def delete_team(team_id: UUID, session: AsyncSession = Depends(get_session), user: User = Depends(get_current_user)):
37 | if not await TeamService(session).is_user_leader(user.id, team_id):
38 | raise HTTPException(
39 | status_code=403,
40 | detail="У вас нет прав на удаление данной команды."
41 | )
42 | return await TeamService(session).repo.delete_by_id(team_id)
43 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/routing/verification_router.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends, HTTPException
2 |
3 | from sqlalchemy.ext.asyncio import AsyncSession
4 |
5 | from app.database.database import get_session
6 | from app.database.models.models import User
7 |
8 |
9 | from app.schema.request.email.confirm_email import EmailConfirmation
10 | from app.schema.request.email.resend_code import ResendCode
11 |
12 | from app.services.user_service import UserService
13 | from app.services.verification_service import VerificationService
14 |
15 | from app.security.jwtmanager import JWTManager, get_current_user, oauth2_scheme
16 |
17 | verification_router = APIRouter(
18 | prefix="/verification",
19 | tags=["Verification"]
20 | )
21 |
22 | @verification_router.post("/resend")
23 | async def resend_verification(email: ResendCode, session: AsyncSession = Depends(get_session)):
24 | user = await UserService(session).get_by_email(email.email)
25 | if not user.success:
26 | raise HTTPException(status_code=400, detail=user.error)
27 |
28 |
29 | result = await VerificationService(session).resend_verification(user.value.id, user.value.email)
30 | if not result.success:
31 | raise HTTPException(status_code=400, detail=result.error)
32 |
33 | return {
34 | "message": "Письмо успешно отправлено."
35 | }
36 | @verification_router.post("/confirm")
37 | async def confirm_verification(confirmRequest: EmailConfirmation, session: AsyncSession = Depends(get_session)):
38 | user = await UserService(session).get_by_email(confirmRequest.email)
39 | if not user.success:
40 | raise HTTPException(status_code=400, detail=user.error)
41 |
42 | result = await VerificationService(session).confirm_email(user.value.id, confirmRequest.code)
43 | if not result.success:
44 | raise HTTPException(status_code=400, detail=result.error)
45 |
46 | verify_user = await UserService(session).confirm_email(user.value.id)
47 |
48 | return {
49 | "message": "Почта успешно подтверждена."
50 | }
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/account/updateprofile.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from pydantic import BaseModel
3 | from typing import Optional
4 |
5 | class UpdateProfile(BaseModel):
6 | surname: Optional[str]
7 | name: Optional[str]
8 | patronymic: Optional[str]
9 | birthdate: Optional[datetime]
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/email/confirm_email.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | class EmailConfirmation(BaseModel):
4 | code: str
5 | email: str
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/email/resend_code.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | class ResendCode(BaseModel):
4 | email: str
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/get_access/authorize.py:
--------------------------------------------------------------------------------
1 | import re
2 | from pydantic import BaseModel, field_validator
3 |
4 | from app.exc import bad_email
5 |
6 | class AuthorizeRequest(BaseModel):
7 | email: str
8 | password: str
9 |
10 | @field_validator("email")
11 | def validate_email(cls, attr):
12 | if not re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", attr):
13 | raise bad_email.BadEmail()
14 | return attr
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/get_access/register.py:
--------------------------------------------------------------------------------
1 | import re
2 | from pydantic import BaseModel, field_validator
3 | from app.exc.bad_email import BadEmail
4 |
5 | class RegisterRequest(BaseModel):
6 | email: str
7 | password: str
8 |
9 | @field_validator("email")
10 | def validate_email(cls, attr):
11 | if not re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", attr):
12 | raise BadEmail()
13 | return attr
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/objective/create_objective.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import Optional
3 | from pydantic import BaseModel
4 |
5 | class CreateObjective(BaseModel):
6 | title: Optional[str]
7 | description: Optional[str]
8 | depends_on: Optional[str]
9 | deadline: Optional[datetime.datetime] = datetime.datetime.now() + datetime.timedelta(days=7)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/organization/create_organization.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pydantic import BaseModel
3 |
4 |
5 | class CreateOrganization(BaseModel):
6 | title: str
7 | description: Optional[str]
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/organization/update_organization.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional
3 |
4 |
5 | class UpdateOrganization(BaseModel):
6 | organizationId: str
7 | new_title: Optional[str] = None
8 | new_description: Optional[str] = None
9 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/project/create_project.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 | from uuid import UUID
3 | from pydantic import BaseModel
4 |
5 | class CreateProject(BaseModel):
6 | title: str
7 | description: Optional[str] = None
8 | auto_invite: List[UUID]
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/project/update_project.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pydantic import BaseModel
3 |
4 | class UpdateProject(BaseModel):
5 | new_title: Optional[str]
6 | new_description: Optional[str]
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/team/create_team.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional
3 | from uuid import UUID
4 |
5 | class CreateTeam(BaseModel):
6 | title: str
7 | description: Optional[str] = None
8 | organizationId: Optional[UUID] = None
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/team/invites/invite_user.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class InviteUser(BaseModel):
5 | user_id: str
6 | team_id: str
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/request/team/update_team.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional
3 |
4 |
5 | class UpdateTeam(BaseModel):
6 | title: str
7 | description: Optional[str] = None
8 | leaderId: Optional[str] = None
9 | organizationId: Optional[str] = None
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/response/AccessToken.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional
3 | class AccessToken(BaseModel):
4 | access_token: str
5 | refresh_token: Optional[str]
6 | token_type: str = "Bearer"
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/response/leader/read_leader.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w1tnessbtwwwww/SpeedSolver/4c34a55114e36d61088c2b18fcd395c72833425f/SpeedSolverBackend/SpeedSolverAPI/app/schema/response/leader/read_leader.py
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/response/team/read_team.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import List, Optional
3 |
4 |
5 |
6 | class ReadTeam(BaseModel):
7 | team: str
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/schema/response/team/team.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pydantic import BaseModel
3 |
4 | class Team(BaseModel):
5 | id: str
6 | title: str
7 | description: Optional[str]
8 | leader: str
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/security/hasher.py:
--------------------------------------------------------------------------------
1 | import bcrypt
2 |
3 |
4 | def hash_password(password: str) -> str:
5 | password_bytes = password.encode('utf-8')
6 |
7 | hashed_password = bcrypt.hashpw(password_bytes, bcrypt.gensalt(5))
8 |
9 | hashed_password_str = hashed_password.decode('utf-8')
10 |
11 | return hashed_password_str
12 |
13 | def verify_password(password: str, hashed_password: str) -> bool:
14 | password_bytes = password.encode('utf-8')
15 | hashed_password_bytes = hashed_password.encode('utf-8')
16 |
17 | return bcrypt.checkpw(password_bytes, hashed_password_bytes)
18 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/security/jwtmanager.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | import datetime
3 | from typing import Optional
4 | from fastapi import Depends, HTTPException
5 | from fastapi.security import OAuth2PasswordBearer
6 |
7 | from jwt import encode, decode
8 |
9 |
10 | from sqlalchemy.ext.asyncio import AsyncSession
11 |
12 | from app.cfg.settings import settings
13 | from app.database.models.models import User
14 | from app.database.repo.user_repository import UserRepository
15 | from app.database.database import get_session
16 |
17 | from app.utils.result import Result, err, success
18 |
19 | from app.security.hasher import verify_password
20 | from app.security.jwttype import JWTType
21 |
22 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v1/access/authorize")
23 |
24 | async def get_current_user(token: str = Depends(oauth2_scheme), session: AsyncSession = Depends(get_session)) -> User:
25 |
26 |
27 | payload = JWTManager().decode_token(token)
28 | if not payload.success:
29 | raise HTTPException(
30 | status_code=401,
31 | detail=payload.error,
32 | headers={"WWW-Authenticate": "Bearer"},
33 | )
34 |
35 | username: str = payload.value.get("userId")
36 | if username is None:
37 | raise HTTPException(
38 | status_code=401,
39 | detail="Could not validate credentials",
40 | headers={"WWW-Authenticate": "Bearer"},
41 | )
42 |
43 | user = await UserRepository(session).get_by_filter_one(id=username)
44 | if user is None:
45 | raise HTTPException(
46 | status_code=401,
47 | detail="User not found",
48 | headers={"WWW-Authenticate": "Bearer"},
49 | )
50 | return user
51 |
52 | class JWTManager:
53 |
54 | def __init__(self):
55 | self.SECRET_KEY = settings.JWT_SECRET_KEY
56 | self.ALGORITHM = settings.JWT_ALGORITHM
57 | self.ACCESS_TOKEN_LIFETIME = settings.JWT_ACCESS_TOKEN_LIFETIME_MINUTES
58 | self.REFRESH_TOKEN_LIFETIME = settings.JWT_REFRESH_TOKEN_LIFETIME_HOURS
59 |
60 | def encode_token(self, payload, token_type: JWTType = JWTType.ACCESS):
61 | jwt_payload = payload.copy()
62 |
63 | current_time = datetime.datetime.utcnow()
64 | expire = timedelta(minutes=self.ACCESS_TOKEN_LIFETIME) if token_type == JWTType.ACCESS else timedelta(hours=self.REFRESH_TOKEN_LIFETIME)
65 | jwt_payload.update({"exp": current_time + expire})
66 | return encode(jwt_payload, self.SECRET_KEY, algorithm=self.ALGORITHM)
67 |
68 | def decode_token(self, token: str) -> Result[dict]:
69 | try:
70 | return success(decode(token, self.SECRET_KEY, algorithms=[self.ALGORITHM]))
71 | except:
72 | return err("Invalid token")
73 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/security/jwttype.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class JWTType(Enum):
5 | ACCESS = "access"
6 | REFRESH = "refresh"
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/organization_service.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | from sqlalchemy import select
5 | from fastapi import HTTPException
6 | from app.database.models.models import Organization, TeamMember
7 | from app.database.repo.organization_repository import OrganizationRepository
8 | from app.schema.request.organization.create_organization import CreateOrganization
9 |
10 |
11 | class OrganizationService:
12 | def __init__(self, session):
13 | self.session = session
14 | self.repo = OrganizationRepository(session)
15 |
16 |
17 | async def get_all_user_organizations(self, user_id: int):
18 | query = (
19 | select(self.repo.model)
20 | .join_from(TeamMember, Organization, TeamMember.userId == user_id)
21 | )
22 | exec = await self.session.execute(query)
23 | result = exec.scalars().all()
24 | return result
25 |
26 | async def create_organization(self, leader_id: int, organization: CreateOrganization):
27 | try:
28 | return await self.repo.create(leaderId=leader_id, **organization.model_dump())
29 | except Exception as e:
30 | raise HTTPException(
31 | status_code=500,
32 | detail=str(e)
33 | )
34 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/project_service.py:
--------------------------------------------------------------------------------
1 | from typing import List, Sequence
2 | from uuid import UUID
3 | from fastapi import HTTPException
4 | from sqlalchemy import and_, select
5 | from sqlalchemy.orm import selectinload, defer
6 | from sqlalchemy.exc import IntegrityError
7 | from sqlalchemy.ext.asyncio import AsyncSession
8 |
9 | from app.database.models.models import Organization, Project, ProjectModerator, Team, TeamMember, User, UserProfile
10 |
11 | from app.database.repo.project_invitation_repository import ProjectInvitationRepository
12 | from app.database.repo.project_members_repository import ProjectMembersRepository
13 | from app.database.repo.project_repository import ProjectRepository
14 | from app.schema.request.project.create_project import CreateProject
15 | from app.services.team_project_service import TeamProjectService
16 | from app.services.team_service import TeamService
17 |
18 | class ProjectService:
19 |
20 | def __init__(self, session: AsyncSession):
21 | self._session = session
22 | self._repo: ProjectRepository = ProjectRepository(session)
23 |
24 |
25 | async def is_user_project_creator(self, project_id: UUID, user_id: UUID):
26 | if await TeamService(self._session).is_user_team_moderator(user_id, project_id):
27 | return True
28 |
29 | query = (
30 | select(Project)
31 | .where(Project.id == project_id)
32 | )
33 |
34 | exec = await self._session.execute(query)
35 | result = exec.scalars().first()
36 |
37 | if result.creator_id == user_id:
38 | return True
39 |
40 | return False
41 |
42 | async def is_user_project_moderator(self, project_id: UUID, user_id: UUID):
43 | if await self.is_user_project_creator(project_id, user_id):
44 | return True
45 |
46 | query = (
47 | select(ProjectModerator)
48 | .where(and_(
49 | ProjectModerator.projectId == project_id,
50 | ProjectModerator.userId == user_id
51 | ))
52 | )
53 |
54 | exec = await self._session.execute(query)
55 | result = exec.scalars().first()
56 |
57 | if result:
58 | return True
59 |
60 | return False
61 |
62 | async def create_project(self, creator_id: UUID, for_team: UUID, project_data: CreateProject):
63 | if await TeamService(self._session).is_user_team_moderator(creator_id, for_team):
64 | try:
65 | project = await self._repo.create(creator_id=creator_id, title=project_data.title, description=project_data.description)
66 | for user_id in project_data.auto_invite:
67 | await ProjectInvitationRepository(self._session).create(invited_user_id=user_id, invited_by_leader_id=creator_id, projectId=project.id)
68 | team_project = await TeamProjectService(self._session).link_project(team_id=for_team, project_id=project.id)
69 | creator_in_project = await ProjectMembersRepository(self._session).create(projectId=project.id, userId=creator_id)
70 | return project
71 | except IntegrityError:
72 | raise HTTPException(
73 | status_code=500,
74 | detail="Произошла ошибка внесения данных в базу данных"
75 | )
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/team_invitation_service.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import List, Sequence
3 | from uuid import UUID
4 | from fastapi import HTTPException
5 | from sqlalchemy import and_, select
6 | from sqlalchemy.orm import selectinload, defer
7 | from sqlalchemy.exc import IntegrityError
8 | from sqlalchemy.ext.asyncio import AsyncSession
9 |
10 | from app.database.models.models import Organization, Project, ProjectModerator, Team, TeamMember, User, UserProfile
11 |
12 | from app.database.repo.project_invitation_repository import ProjectInvitationRepository
13 | from app.database.repo.project_members_repository import ProjectMembersRepository
14 | from app.database.repo.project_repository import ProjectRepository
15 | from app.database.repo.team_invitation_repository import TeamInvitationRepository
16 | from app.schema.request.project.create_project import CreateProject
17 | from app.services.team_project_service import TeamProjectService
18 | from app.services.team_service import TeamService
19 |
20 | class ProjectService:
21 |
22 | def __init__(self, session: AsyncSession):
23 | self._session = session
24 | self._repo: TeamInvitationRepository = TeamInvitationRepository(session)
25 |
26 | async def invite_user(self, team_id: UUID, user_id: UUID, moderator_id: UUID):
27 | if not await TeamService(self._session).is_user_team_moderator(moderator_id, team_id):
28 | raise HTTPException (
29 | status_code=403,
30 | detail="У вас нет прав на приглашение в команду."
31 | )
32 |
33 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/team_project_service.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 |
3 | from fastapi import HTTPException
4 | from sqlalchemy import and_, select
5 |
6 | from app.database.repo.team_project_repository import TeamProjectRepository
7 | from app.utils.logger.telegram_bot.telegram_logger import logger
8 | class TeamProjectService:
9 | def __init__(self, session):
10 | self.session = session
11 | self.repo = TeamProjectRepository(session)
12 |
13 |
14 | async def link_project(self, team_id: UUID, project_id: UUID):
15 | return await self.repo.create(teamId=team_id, projectId=project_id)
16 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/team_service.py:
--------------------------------------------------------------------------------
1 | from uuid import UUID
2 |
3 | from fastapi import HTTPException
4 | from sqlalchemy import and_, select
5 |
6 | from app.database.models.models import TeamMember, TeamModerator, User
7 | from app.database.repo.team_invitation_repository import TeamInvitationRepository
8 | from app.database.repo.team_member_repository import TeamMemberRepository
9 | from app.database.repo.team_repository import TeamRepository
10 | from app.schema.request.team.create_team import CreateTeam
11 | from app.schema.request.team.update_team import UpdateTeam
12 | from sqlalchemy.exc import IntegrityError
13 | from app.utils.logger.telegram_bot.telegram_logger import logger
14 | class TeamService:
15 | def __init__(self, session):
16 | self.session = session
17 | self.repo = TeamRepository(session)
18 |
19 | async def invite_user(self, team_id: UUID, user_id: UUID, moderator_id: UUID):
20 | if not await self.is_user_team_moderator(moderator_id, team_id):
21 | raise HTTPException(
22 | status_code=403,
23 | detail="У вас нет прав на приглашение в команду."
24 | )
25 |
26 | await TeamInvitationRepository(self.session).create(teamId=team_id, userId=user_id)
27 |
28 | async def is_user_leader(self, user_id: UUID, team_id: UUID) -> bool:
29 | team = await self.repo.get_by_id(team_id)
30 | if team.leaderId == user_id:
31 | return True
32 | return False
33 |
34 | async def is_user_team_moderator(self, user_id: UUID, team_id: UUID) -> bool:
35 |
36 | if await self.is_user_leader(user_id, team_id):
37 | return True
38 |
39 | moderator_query = (
40 | select(TeamModerator)
41 | .where(and_(
42 | TeamModerator.userId == user_id,
43 | TeamModerator.teamId == team_id
44 | ))
45 | )
46 |
47 | exec = await self.session.execute(moderator_query)
48 | moderator_row = exec.scalars().first()
49 |
50 | if moderator_row:
51 | return True
52 |
53 | return False
54 |
55 | async def update_team(self, team_id: UUID, updates: UpdateTeam):
56 | return await self.repo.update_by_id(team_id, **updates.model_dump())
57 |
58 | async def create_team(self, team: CreateTeam, leader_id: UUID):
59 | try:
60 | created_team = await self.repo.create(leaderId=leader_id, **team.model_dump())
61 | await TeamMemberRepository(self.session).create(teamId=created_team.id, userId=leader_id)
62 | except IntegrityError as e:
63 | logger.error("Произошла ошибка внесения данных в базу данных", str(e))
64 | raise HTTPException(
65 | status_code=500,
66 | detail="Произошла ошибка внесения данных в базу данных"
67 | )
68 |
69 |
70 |
71 | async def get_all_user_teams(self, user_id: UUID):
72 | query = (
73 | select(self.repo.model)
74 | .join_from(TeamMember, User, TeamMember.userId == user_id)
75 | )
76 | exec = await self.session.execute(query)
77 | result = exec.scalars().all()
78 | return result
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/user_profile_service.py:
--------------------------------------------------------------------------------
1 |
2 | from sqlalchemy.exc import IntegrityError
3 | from sqlalchemy.ext.asyncio import AsyncSession
4 |
5 | from app.database.models.models import User
6 | from app.database.repo.user_profile_repository import UserProfileRepository
7 |
8 | from app.schema.request.get_access import authorize, register
9 | from app.schema.request.account.updateprofile import UpdateProfile
10 |
11 | from app.security.hasher import hash_password, verify_password
12 | from app.security.jwtmanager import JWTManager
13 |
14 | from app.utils.result import Result, err, success
15 |
16 | class UserProfileService:
17 |
18 | def __init__(self, session: AsyncSession):
19 | self._session = session
20 | self._repo: UserProfileRepository = UserProfileRepository(session)
21 |
22 |
23 | async def update_profile(self, userId: str, update_request: UpdateProfile):
24 | return await self._repo.update_profile(
25 | userId,
26 | update_request.surname,
27 | update_request.name,
28 | update_request.patronymic,
29 | update_request.birthdate
30 | )
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/user_service.py:
--------------------------------------------------------------------------------
1 |
2 | from typing import List, Sequence
3 | from uuid import UUID
4 | from sqlalchemy import select
5 | from sqlalchemy.orm import selectinload, defer
6 | from sqlalchemy.exc import IntegrityError
7 | from sqlalchemy.ext.asyncio import AsyncSession
8 |
9 | from app.database.models.models import Organization, Team, TeamMember, User, UserProfile
10 |
11 | from app.database.repo.user_repository import UserRepository
12 |
13 | from app.schema.request.get_access import authorize, register
14 | from app.schema.request.account.updateprofile import UpdateProfile
15 | from app.schema.response.AccessToken import AccessToken
16 |
17 | from app.services.verification_service import VerificationService
18 | from app.utils.email_service.email_service import EmailService
19 | from app.utils.result import Result, err, success
20 |
21 | from app.security.hasher import hash_password, verify_password
22 |
23 | from app.security.jwtmanager import JWTManager
24 | from app.security.jwttype import JWTType
25 |
26 |
27 | class UserService:
28 |
29 | def __init__(self, session: AsyncSession):
30 | self._session = session
31 | self._repo: UserRepository = UserRepository(session)
32 |
33 |
34 | async def get_all_teams(self, user_id: UUID):
35 | query = (
36 | select(Team)
37 | .options(defer(Team.leaderId))
38 | .select_from(TeamMember)
39 | .where(TeamMember.userId == user_id)
40 | .options(selectinload(Team.leader),
41 | selectinload(Team.organization).defer(Organization.id))
42 | )
43 |
44 | exec = await self._session.execute(query)
45 | result = exec.scalars().all()
46 | return result
47 |
48 | async def get_moderation_teams(self, userId: str):
49 | return await self._repo.get_moderation_teams(userId)
50 |
51 | async def delete_account(self, userId: str):
52 | return await self._repo.delete_by_id(userId)
53 |
54 | async def get_by_email(self, email: str):
55 | user = await self._repo.get_by_email(email)
56 | return success(user) if user else err("Пользователь не найден.")
57 |
58 | async def confirm_email(self, userId: str):
59 | return await self._repo.update_by_id(userId, is_mail_verified=True)
60 |
61 | async def update_profile(self, token: str, update_request: UpdateProfile):
62 | raise NotImplementedError
63 |
64 | async def register(self, register_request: register.RegisterRequest) -> Result[None]:
65 | try:
66 | inserted = await self._repo.create(email=register_request.email, password=hash_password(register_request.password))
67 | is_verification_inserted = await VerificationService(self._session).process_verification(inserted.id, inserted.email)
68 | if not is_verification_inserted.success:
69 | await self._repo.rollback()
70 | return err(is_verification_inserted.error)
71 |
72 | await self._repo.commit()
73 | except IntegrityError as e:
74 | return err(str(e))
75 | return success("Пользователь успешно зарегистрирован. Проверьте почту.")
76 |
77 | async def authorize(self, email: str, password: str):
78 | authenticated = await self._repo.authenticate_user(email, password)
79 | if not authenticated.success:
80 | return err("Неправильный логин или пароль")
81 |
82 | if not authenticated.value.is_mail_verified:
83 | return err("Почта не подтверждена. Если кода нет, запросите его повторно")
84 | return success(authenticated.value)
85 |
86 | async def is_email_verified(self, username: str) -> Result[None]:
87 | user = await self._repo.get_by_filter_one(email=username)
88 | if not user:
89 | return err("Пользователь не найден.")
90 | if not user.is_mail_verified:
91 | return err("Почта не подтверждена. Если кода нет, запросите его повторно")
92 | return success("Почта подтверждена.")
93 |
94 |
95 | async def delete_profile(self, token: str):
96 | user: User = await JWTManager().get_current_user(token, self._session)
97 | return await self._repo.delete_by_id(user.userId)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/services/verification_service.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.asyncio import AsyncSession
2 |
3 | from app.database.repo.verification_repository import VerificationRepository
4 | from app.utils.result import Result, err, success
5 | from app.utils.email_service.email_service import EmailService
6 | from app.utils.logger.telegram_bot.telegram_logger import logger
7 | from app.utils.verify_codes_generator.code_generator import generate_confirmation_code
8 | from app.security.jwtmanager import JWTManager
9 | class VerificationService:
10 | def __init__(self, session: AsyncSession):
11 | self._session = session
12 | self._repo: VerificationRepository = VerificationRepository(session)
13 |
14 |
15 | async def process_verification(self, userId: str, email: str):
16 | code = await EmailService().send_verify_code("Подтверждение письма", email)
17 | if not code.success:
18 | return err("Не удалось отправить код. Возможно, неверная почта.")
19 |
20 | verification = await self._repo.process_verification(userId, code.value)
21 | return verification
22 |
23 | async def resend_verification(self, userId: str, email: str):
24 | code = await EmailService().send_verify_code("Подтверждение письма", email)
25 |
26 | if not code.success:
27 | logger.fatal(f"Произошла ошибка в переотправке кода. {code.error}")
28 | return
29 |
30 | return await self._repo.resend_verification(userId, code.value)
31 |
32 | async def confirm_email(self, userId: str, code: str) -> Result[None]:
33 | return await self._repo.confirm_email(userId, code)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/utils/email_service/email_service.py:
--------------------------------------------------------------------------------
1 | from email.mime.text import MIMEText
2 | from email.mime.multipart import MIMEMultipart
3 | import smtplib
4 | from app.cfg.settings import settings
5 | from app.utils.result import Result, err, success
6 | from app.utils.verify_codes_generator.code_generator import generate_confirmation_code
7 | from app.utils.logger.telegram_bot.telegram_logger import logger
8 | class EmailService:
9 |
10 | @staticmethod
11 | async def send_verify_code(subject: str, send_to: str) -> Result[str]:
12 | smtp_server = 'smtp.mail.ru'
13 | smtp_port = 587
14 |
15 | print(settings.MAIL_EMAIL)
16 | smtp_username = settings.MAIL_EMAIL
17 | smtp_password = settings.MAIL_PASSWORD
18 |
19 | from_addr = settings.MAIL_EMAIL
20 | to_addr = send_to
21 | # Отправитель и получатель
22 |
23 | # Создание сообщения
24 | msg = MIMEMultipart()
25 | msg['From'] = from_addr
26 | msg['To'] = to_addr
27 | msg['Subject'] = subject
28 |
29 |
30 | code = generate_confirmation_code()
31 | # Текст сообщения
32 | msg.attach(MIMEText(f"Здравствуйте!\nВаш код подтверждения для регистрации в сервисе SpeedSolver: {code}"))
33 |
34 | result = None
35 |
36 | # Отправка сообщения
37 | try:
38 | server = smtplib.SMTP(smtp_server, smtp_port)
39 | server.starttls()
40 | server.login(smtp_username, smtp_password)
41 | text = msg.as_string()
42 | server.sendmail(from_addr, to_addr, text)
43 | print('Сообщение успешно отправлено')
44 | result = success(code)
45 | logger.info(f"sended code {code} to {send_to}")
46 | except Exception as e:
47 | result = err(str(e))
48 | logger.error(f'Ошибка при отправке кода на почту.', str(e))
49 | finally:
50 | server.quit()
51 | return result
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/utils/logger/logger.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | class Logger(ABC):
4 |
5 | @abstractmethod
6 | async def send_log(message: str = "Empty Log"):
7 | ...
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/utils/logger/telegram_bot/telegram_logger.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import Optional
3 | from app.cfg.settings import settings
4 |
5 | from app.utils.logger.logger import Logger
6 |
7 | import telebot
8 |
9 | from app.utils.result import Result, err, success
10 |
11 | class TelegramLogger(Logger):
12 | def __init__(self,
13 | token: str = settings.TELEGRAM_API_TOKEN,
14 | chat_id: str = settings.TELEGRAM_CHAT_ID):
15 | self._bot = telebot.TeleBot(token)
16 | self._chat_id = chat_id
17 |
18 |
19 | def send_log(self, template: str) -> Result[None]:
20 | try:
21 | self._bot.send_message(self._chat_id, template, parse_mode="Markdown")
22 | return success()
23 | except:
24 | return err("Произошла ошибка")
25 |
26 | def info(self, message: str):
27 | template = f"❗**INFO**❗\n{message}\nDate: {datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")}"
28 |
29 | self.send_log(template)
30 |
31 |
32 | def error(self, message: str, traceback: Optional[str] = None) -> str:
33 |
34 | if traceback:
35 | template = f"🚨 **ERROR** 🚨\n{message}\n```python\n{traceback}```\nDate: {datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')}"
36 | else:
37 | template = f"🚨 **ERROR** 🚨\n{message}\nDate: {datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")}"
38 |
39 | self.send_log(template)
40 |
41 | def warning(self, message: str) -> str:
42 | template = f"⚠️ **WARNING** ⚠️\n{message}\nDate: {datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")}"
43 |
44 | self.send_log(template)
45 |
46 | def fatal(self, message: str, traceback: str = None) -> str:
47 |
48 | if traceback:
49 | template = f"🆘 **FATAL** 🆘\n{message}\n```python\n{traceback}```\nDate: {datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')}"
50 | else:
51 | template = f"🆘 **FATAL** 🆘\n{message}\nDate: {datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")}"
52 |
53 | self.send_log(template)
54 |
55 | logger: TelegramLogger = TelegramLogger()
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/utils/result.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | from typing import Generic, TypeVar, Optional
4 |
5 | T = TypeVar('T')
6 |
7 |
8 | class Result(Generic[T]):
9 | def __init__(self, success: bool, value: T | None = None, error: str | None = None):
10 | self.success = success
11 | self.value = value
12 | self.error = error
13 |
14 | def err(msg: str) -> Result[None]: return Result(success=False, error=msg)
15 |
16 | def success(value: T) -> Result[T]: return Result(success=True, value=value)
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/app/utils/verify_codes_generator/code_generator.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | def generate_confirmation_code(length=8):
5 | return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/docker/Dockerfile.backend:
--------------------------------------------------------------------------------
1 | # Используем официальный образ Python
2 | FROM python:3.13-slim-bullseye AS prod
3 |
4 | # Устанавливаем необходимые зависимости
5 | RUN apt-get update && apt-get install -y \
6 | gcc \
7 | curl \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | # Устанавливаем Poetry через pip
11 | RUN pip install poetry==1.8.2
12 |
13 | # Создаем директории для проекта
14 | WORKDIR /app/speedsolver
15 |
16 | # Копируем pyproject.toml и poetry.lock
17 | COPY pyproject.toml poetry.lock /app/speedsolver/
18 |
19 | # Настраиваем Poetry
20 | RUN poetry config virtualenvs.create false
21 |
22 | # Устанавливаем зависимости
23 | RUN poetry install --only main
24 |
25 | # Очищаем кэш
26 | RUN apt-get purge -y && rm -rf /var/lib/apt/lists/*
27 |
28 | # Копируем приложение
29 | COPY . /app/speedsolver/
30 |
31 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/docker/docker-compose.backend.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 |
5 | speedsolver.database:
6 | container_name: speedsolver_database
7 | image: postgres:16.4
8 | restart: always
9 | env_file:
10 | - .env
11 | environment:
12 | - POSTGRES_USER=${POSTGRES_USER}
13 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
14 | - POSTGRES_DB=${POSTGRES_DB}
15 | ports:
16 | - ${POSTGRES_PORT}:5432
17 | volumes:
18 | - /var/lib/postgresql/data
19 |
20 | speedsolver_api:
21 | container_name: speedsolver_api
22 | build:
23 | context: .
24 | dockerfile: docker/Dockerfile.backend
25 | restart: always
26 | command: ["poetry", "run", "uvicorn", "main:api", "--host", "0.0.0.0", "--port", "8000"]
27 | env_file:
28 | - .env
29 | ports:
30 | - ${API_BASE_PORT}:8000
31 |
32 | depends_on:
33 | - speedsolver.database
34 |
35 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/main.py:
--------------------------------------------------------------------------------
1 | from contextlib import asynccontextmanager
2 | from fastapi import Depends, FastAPI, HTTPException
3 | from fastapi.responses import JSONResponse
4 | from fastapi.responses import RedirectResponse as Redirect
5 | from pydantic import BaseModel
6 |
7 | from app.cfg.settings import settings
8 | from app.database.database import get_session, create_tables
9 | from app.exc.bad_email import BadEmail
10 | from app.routing.main_router import main_router
11 | from app.services.user_service import UserService
12 | from app.utils.email_service.email_service import EmailService
13 |
14 | from starlette.middleware.cors import CORSMiddleware
15 |
16 | from typing import Annotated
17 | from sqlalchemy.ext.asyncio import AsyncSession
18 | from alembic.config import Config
19 | from alembic import command
20 |
21 |
22 |
23 | api = FastAPI(
24 | title="SpeedSolverAPI",
25 | description="The API docs for SpeedSolver.",
26 | version="v1",
27 | )
28 |
29 |
30 | api.add_middleware (
31 | CORSMiddleware,
32 | allow_origins=[
33 | "http://speedsolver.ru",
34 | "https://speedsolver.ru",
35 | "http://localhost:3001"
36 | ],
37 | allow_credentials=True,
38 | allow_methods=["*"],
39 | allow_headers=["*"]
40 | )
41 |
42 |
43 | api.include_router(main_router)
44 |
45 |
46 | @api.exception_handler(BadEmail)
47 | async def bad_email(request, exc: BadEmail):
48 | raise HTTPException(
49 | status_code=422,
50 | detail=exc.message
51 | )
52 |
53 | @api.on_event("startup")
54 | async def startup_event():
55 | await create_tables()
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "speedsolverapi"
3 | version = "0.1.0"
4 | description = ""
5 | authors = ["w1tnessbtwwwww "]
6 | readme = "README.md"
7 |
8 | [tool.poetry.dependencies]
9 | python = "^3.13"
10 | fastapi = "^0.115.6"
11 | pydantic = "^2.10.4"
12 | pydantic-settings = "^2.7.0"
13 | uvicorn = "^0.34.0"
14 | greenlet = "^3.1.1"
15 | asyncpg = "^0.30.0"
16 | sqlalchemy = "^2.0.36"
17 | pyjwt = "^2.10.1"
18 | yarl = "^1.18.3"
19 | bcrypt = "^4.2.1"
20 | pytelegrambotapi = "^4.26.0"
21 | python-multipart = "^0.0.20"
22 | alembic = "^1.14.0"
23 | multipledispatch = "^1.0.0"
24 |
25 |
26 | [build-system]
27 | requires = ["poetry-core"]
28 | build-backend = "poetry.core.masonry.api"
29 |
--------------------------------------------------------------------------------
/SpeedSolverBackend/SpeedSolverAPI/tests/telegram_logger_test.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w1tnessbtwwwww/SpeedSolver/4c34a55114e36d61088c2b18fcd395c72833425f/SpeedSolverBackend/SpeedSolverAPI/tests/telegram_logger_test.py
--------------------------------------------------------------------------------
/SpeedSolverFrontend/.env.example:
--------------------------------------------------------------------------------
1 | VITE_SPEEDSOLVER_DEPLOY_API_URL=
2 | VITE_SPEEDSOLVER_LOCAL_API_URL=
--------------------------------------------------------------------------------
/SpeedSolverFrontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env
--------------------------------------------------------------------------------
/SpeedSolverFrontend/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/docker/Dockerfile.frontend:
--------------------------------------------------------------------------------
1 | # Используем официальный Node.js образ
2 | FROM node:20 AS build
3 |
4 | # Устанавливаем рабочую директорию
5 | WORKDIR /app/frontend
6 |
7 | # Копируем package.json и package-lock.json
8 | COPY package*.json ./
9 |
10 | # Устанавливаем зависимости
11 | RUN npm install
12 |
13 | # Копируем весь проект в контейнер
14 | COPY . .
15 |
16 | # Строим проект
17 | RUN npm run build
18 |
19 | # Используем Nginx для продакшн сервера
20 | FROM nginx:alpine
21 |
22 |
23 | COPY nginx.conf /etc/nginx/nginx.conf
24 | COPY --from=build /app/frontend/dist /usr/share/nginx/html
25 |
26 | # Открываем порт 80
27 | EXPOSE 3000
28 |
29 |
30 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/SpeedSolverFrontend/docker/docker-compose.frontend.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | web:
5 | container_name: speedsolverfrontend
6 | build:
7 | context: .
8 | dockerfile: docker/Dockerfile.frontend
9 | ports:
10 | - 3001:80
11 | environment:
12 | - VITE_PORT=3000
--------------------------------------------------------------------------------
/SpeedSolverFrontend/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SpeedSolver
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/nginx.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes 1;
3 |
4 | error_log /var/log/nginx/error.log warn;
5 | pid /var/run/nginx.pid;
6 |
7 | events {
8 | worker_connections 1024;
9 | }
10 |
11 | http {
12 | include /etc/nginx/mime.types;
13 | default_type application/octet-stream;
14 |
15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
16 | '$status $body_bytes_sent "$http_referer" '
17 | '"$http_user_agent" "$http_x_forwarded_for"';
18 |
19 | access_log /var/log/nginx/access.log main;
20 |
21 | sendfile on;
22 | keepalive_timeout 65;
23 |
24 | server {
25 | listen 80;
26 | server_name localhost;
27 |
28 | location / {
29 | root /usr/share/nginx/html;
30 | index index.html index.htm;
31 | try_files $uri $uri/ /index.html;
32 | }
33 |
34 | error_page 404 /404.html;
35 | location = /40x.html {
36 | }
37 |
38 | error_page 500 502 503 504 /50x.html;
39 | location = /50x.html {
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "speedsolverfrontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@tailwindcss/vite": "^4.1.3",
14 | "axios": "^1.7.9",
15 | "class-variance-authority": "^0.7.1",
16 | "clsx": "^2.1.1",
17 | "lucide-react": "^0.488.0",
18 | "react": "^18.3.1",
19 | "react-dom": "^18.3.1",
20 | "react-modal": "^3.16.3",
21 | "react-router-dom": "^7.0.2",
22 | "react-spring": "^9.7.5",
23 | "react-toastify": "^10.0.6",
24 | "tailwind-merge": "^3.2.0",
25 | "tailwindcss": "^4.1.3",
26 | "tw-animate-css": "^1.2.5"
27 | },
28 | "devDependencies": {
29 | "@eslint/js": "^9.15.0",
30 | "@types/node": "^22.14.1",
31 | "@types/react": "^18.3.12",
32 | "@types/react-dom": "^18.3.1",
33 | "@vitejs/plugin-react": "^4.3.4",
34 | "eslint": "^9.15.0",
35 | "eslint-plugin-react-hooks": "^5.0.0",
36 | "eslint-plugin-react-refresh": "^0.4.14",
37 | "globals": "^15.12.0",
38 | "typescript": "~5.6.2",
39 | "typescript-eslint": "^8.15.0",
40 | "vite": "^6.2.6"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w1tnessbtwwwww/SpeedSolver/4c34a55114e36d61088c2b18fcd395c72833425f/SpeedSolverFrontend/src/App.css
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from "react-router-dom"
2 | import WelcomePage from "./pages/welcome/WelcomePage"
3 | import { LoginPage } from "./pages/access/login/LoginPage"
4 | import { RegisterPage } from "./pages/access/register/RegisterPage"
5 | import { Dashboard } from "./pages/dashboard/Dashboard"
6 | import Teams from "./pages/dashboard/Teams"
7 | import Companies from "./pages/dashboard/Companies"
8 | import Projects from "./pages/dashboard/Projects"
9 | import TestPage from "./pages/test/TestPage"
10 | import DashboardLayout from '@/layout/DashboardLayout'
11 | import AboutPage from "./pages/aboutUs/aboutPage"
12 |
13 | function App() {
14 | return (
15 |
16 | } />
17 | } />
18 | } />
19 | } />
20 |
21 | }>
22 | } />
23 | } />
24 | } />
25 | }/>
26 |
27 | } />
28 |
29 | )
30 | }
31 |
32 | export default App
33 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/app/axios_api.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getCookie } from "../utils/cookieUtils";
3 |
4 |
5 | const client = axios.create({
6 | baseURL: import.meta.env.VITE_SPEEDSOLVER_DEPLOY_API_URL
7 | })
8 |
9 | // client.interceptors.response.use (
10 | // response => {
11 | // return response
12 | // },
13 | // error => {
14 | // if (error.response && error.response.status === 401) {
15 | // return refreshToken()
16 | // }
17 | // }
18 | // )
19 |
20 |
21 | export const get_all_teams = () => {
22 | console.log(getCookie("access_token"))
23 | return client.get("/account/teams/get_all", {
24 | headers: {
25 | Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIwNTVjNjdiNS1lNTc5LTQ1MzEtOTIzOC0xYjVlOGUzNDRlNWEiLCJleHAiOjE3NDQ0NTU2NTF9.Neoa88txlnadWifRplHS9_gFixTC9Tqb4fEUHOfRDH4`
26 | }
27 | })
28 | .then(response => {
29 | console.log(response.data)
30 | return response.data
31 | })
32 | .catch(error => console.log(error))
33 | }
34 |
35 | export const refreshToken = () => {
36 | return client.get("/access/refresh")
37 | .then()
38 | .catch(error => Promise.reject(error))
39 | }
40 |
41 | export const authorize = (username: string, password: string) => {
42 | return client.post("/access/authorize", `username=${username}&password=${password}`, {
43 | headers: {
44 | "Content-Type": "application/x-www-form-urlencoded"
45 | }
46 | }).then(response => response.data)
47 | .catch(error => {
48 | if (error.response) {
49 | return Promise.reject(error.response.data.detail) || "Неизвестная ошибка"
50 | }
51 | return Promise.reject("Ошибка на стороне клиента.")
52 | })
53 | }
54 |
55 | export const register = (username: string, password: string) => {
56 | return client.post("/access/register", {
57 | username: username,
58 | password: password
59 | }).then(response => response.data)
60 | .catch(error => {
61 | if (error.response) {
62 | return Promise.reject(error.response.data.detail) || "Неизвестная ошибка"
63 | }
64 | return Promise.reject("Ошибка на стороне клиента.")
65 | })
66 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/assets/mainbackground.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/card/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 |
3 | const Card = ({children, className}:{children:ReactNode, className?:string}) => {
4 | return (
5 |
6 | {children}
7 |
8 | )
9 | }
10 |
11 | export default Card
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/dashboardNavigation/dashboardNavigation.css:
--------------------------------------------------------------------------------
1 |
2 | .headerNavigation {
3 | display: flex;
4 | flex-direction: row;
5 | gap: 30px;
6 | justify-content: start;
7 | align-items: start;
8 | margin-top: 40px;
9 | align-self: flex-start;
10 | margin-left: 50px;
11 | }
12 |
13 | .headerNavigation__buttons {
14 | display: flex;
15 | gap: 15px;
16 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/dashboardNavigation/dashboardNavigation.tsx:
--------------------------------------------------------------------------------
1 | import "./dashboardNavigation.css"
2 |
3 | export const DashboardNavigation = () => {
4 | return (
5 |
6 |
7 |
8 | Мои организации
9 |
10 | {
13 | window.location.href = "/teams"
14 | }}>
15 | Мои команды
16 |
17 |
18 | Мои проекты
19 |
20 |
21 |
22 | )
23 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/passwordInput/PasswordInput.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | width: fit-content;
3 | display: flex;
4 | flex-direction: row;
5 | align-items: center;
6 | border-radius: var(--input-border-radius);
7 | overflow: hidden;
8 | background-color: black;
9 | color: white;
10 | padding-right: var(--input-padding);
11 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/passwordInput/PasswordInput.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef, forwardRef, useImperativeHandle } from "react";
2 | import { Eye, EyeClosed } from 'lucide-react';
3 | import styles from './PasswordInput.module.css';
4 |
5 | const PasswordInput = forwardRef((props, ref) => {
6 | const [showPassword, setShowPassword] = useState(false);
7 | const [isFocused, setIsFocused] = useState(false);
8 | const inputField = useRef(null);
9 |
10 | const toggleShowPassword = (e: React.MouseEvent) => {
11 | e.preventDefault(); // Предотвращаем действие по умолчанию
12 | setShowPassword(c => !c);
13 | };
14 |
15 | const handleFocus = () => {
16 | setIsFocused(true);
17 | };
18 |
19 | const handleBlur = () => {
20 | setIsFocused(false);
21 | };
22 |
23 | useImperativeHandle(ref, () => ({
24 | get value() {
25 | return inputField.current ? inputField.current.value : '';
26 | },
27 | focus() {
28 | if (inputField.current) {
29 | inputField.current.focus();
30 | }
31 | },
32 | }));
33 |
34 | return (
35 |
36 |
45 |
46 | {showPassword ? : }
47 |
48 |
49 | );
50 | });
51 |
52 | export default PasswordInput;
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/swappingText/SwappingText.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useMemo } from 'react';
2 | import { useSpring, animated } from 'react-spring';
3 | import './TextStyles.css';
4 |
5 | const AnimatedText = ({strings}:{strings:string[]}) => {
6 | const [text, setText] = useState('');
7 | const [index, setIndex] = useState(0);
8 |
9 | // Используем useMemo для мемоизации массива texts
10 | const texts = useMemo(() => strings, []);
11 |
12 | useEffect(() => {
13 | const interval = setInterval(() => {
14 | setIndex((prevIndex) => (prevIndex + 1) % texts.length);
15 | }, 1500); // Меняйте текст каждые 1.5 секунды
16 |
17 | return () => clearInterval(interval);
18 | }, [texts.length]);
19 |
20 | useEffect(() => {
21 | setText(texts[index]);
22 | }, [index, texts]);
23 |
24 | const springProps = useSpring({
25 | opacity: 0.6,
26 | from: { opacity: 0 },
27 | config: { duration: 1000 },
28 | reset: true,
29 | });
30 |
31 | return (
32 |
33 | {text}
34 |
35 | );
36 | };
37 |
38 | export default AnimatedText;
39 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/swappingText/TextStyles.css:
--------------------------------------------------------------------------------
1 | .welcome-text {
2 | color: white;
3 | text-align: center;
4 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/teamslist/TeamsList.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .teamsList {
4 | background-color: blueviolet;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | width: 30vh;
10 | height: 150px;
11 | border-radius: 10px;
12 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/components/teamslist/TeamsList.tsx:
--------------------------------------------------------------------------------
1 | import { Team } from "../../types/teams/types"
2 | import "./TeamsList.css"
3 |
4 | interface TeamListProps {
5 | teams: Team[]
6 | }
7 |
8 | export const TeamsList: React.FC = ({ teams }) => {
9 | return (
10 |
11 | {teams.map((team) => (
12 |
13 |
{team.title}
14 |
{team.description}
15 |
16 | ))}
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
2 | @import "tailwindcss";
3 | @import "tw-animate-css";
4 |
5 | @custom-variant dark (&:is(.dark *));
6 |
7 | @theme inline {
8 | --radius-sm: calc(var(--radius) - 4px);
9 | --radius-md: calc(var(--radius) - 2px);
10 | --radius-lg: var(--radius);
11 | --radius-xl: calc(var(--radius) + 4px);
12 | --color-background: var(--background);
13 | --color-foreground: var(--foreground);
14 | --color-card: var(--card);
15 | --color-card-foreground: var(--card-foreground);
16 | --color-popover: var(--popover);
17 | --color-popover-foreground: var(--popover-foreground);
18 | --color-primary: var(--primary);
19 | --color-primary-foreground: var(--primary-foreground);
20 | --color-secondary: var(--secondary);
21 | --color-secondary-foreground: var(--secondary-foreground);
22 | --color-muted: var(--muted);
23 | --color-muted-foreground: var(--muted-foreground);
24 | --color-accent: var(--accent);
25 | --color-accent-foreground: var(--accent-foreground);
26 | --color-destructive: var(--destructive);
27 | --color-border: var(--border);
28 | --color-input: var(--input);
29 | --color-ring: var(--ring);
30 | --color-chart-1: var(--chart-1);
31 | --color-chart-2: var(--chart-2);
32 | --color-chart-3: var(--chart-3);
33 | --color-chart-4: var(--chart-4);
34 | --color-chart-5: var(--chart-5);
35 | --color-sidebar: var(--sidebar);
36 | --color-sidebar-foreground: var(--sidebar-foreground);
37 | --color-sidebar-primary: var(--sidebar-primary);
38 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
39 | --color-sidebar-accent: var(--sidebar-accent);
40 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
41 | --color-sidebar-border: var(--sidebar-border);
42 | --color-sidebar-ring: var(--sidebar-ring);
43 |
44 |
45 | }
46 |
47 | :root {
48 | --radius: 0.625rem;
49 | --background: oklch(1 0 0);
50 | --foreground: oklch(0.145 0 0);
51 | --card: oklch(1 0 0);
52 | --card-foreground: oklch(0.145 0 0);
53 | --popover: oklch(1 0 0);
54 | --popover-foreground: oklch(0.145 0 0);
55 | --primary: oklch(0.205 0 0);
56 | --primary-foreground: oklch(0.985 0 0);
57 | --secondary: oklch(0.97 0 0);
58 | --secondary-foreground: oklch(0.205 0 0);
59 | --muted: oklch(0.97 0 0);
60 | --muted-foreground: oklch(0.556 0 0);
61 | --accent: oklch(0.97 0 0);
62 | --accent-foreground: oklch(0.205 0 0);
63 | --destructive: oklch(0.577 0.245 27.325);
64 | --border: oklch(0.922 0 0);
65 | --input: oklch(0.922 0 0);
66 | --ring: oklch(0.708 0 0);
67 | --chart-1: oklch(0.646 0.222 41.116);
68 | --chart-2: oklch(0.6 0.118 184.704);
69 | --chart-3: oklch(0.398 0.07 227.392);
70 | --chart-4: oklch(0.828 0.189 84.429);
71 | --chart-5: oklch(0.769 0.188 70.08);
72 | --sidebar: oklch(0.985 0 0);
73 | --sidebar-foreground: oklch(0.145 0 0);
74 | --sidebar-primary: oklch(0.205 0 0);
75 | --sidebar-primary-foreground: oklch(0.985 0 0);
76 | --sidebar-accent: oklch(0.97 0 0);
77 | --sidebar-accent-foreground: oklch(0.205 0 0);
78 | --sidebar-border: oklch(0.922 0 0);
79 | --sidebar-ring: oklch(0.708 0 0);
80 | --input-padding: 10px;
81 | --input-border-color: #ccc;
82 | --input-focus-border-color: gray;
83 | --input-border-radius: 12px;
84 | --font-inter: 'Inter', sans-serif;
85 | --color-primary: #8f297a;
86 | --color-secondary: #6d2e8f;
87 | --color-third: #52508f;
88 | --card-background: #0a0a0a;
89 | --card-border: #818181;
90 | --spacing-base:20px;
91 | }
92 |
93 | .dark {
94 | --background: oklch(0.145 0 0);
95 | --foreground: oklch(0.985 0 0);
96 | --card: oklch(0.205 0 0);
97 | --card-foreground: oklch(0.985 0 0);
98 | --popover: oklch(0.205 0 0);
99 | --popover-foreground: oklch(0.985 0 0);
100 | --primary: oklch(0.922 0 0);
101 | --primary-foreground: oklch(0.205 0 0);
102 | --secondary: oklch(0.269 0 0);
103 | --secondary-foreground: oklch(0.985 0 0);
104 | --muted: oklch(0.269 0 0);
105 | --muted-foreground: oklch(0.708 0 0);
106 | --accent: oklch(0.269 0 0);
107 | --accent-foreground: oklch(0.985 0 0);
108 | --destructive: oklch(0.704 0.191 22.216);
109 | --border: oklch(1 0 0 / 10%);
110 | --input: oklch(1 0 0 / 15%);
111 | --ring: oklch(0.556 0 0);
112 | --chart-1: oklch(0.488 0.243 264.376);
113 | --chart-2: oklch(0.696 0.17 162.48);
114 | --chart-3: oklch(0.769 0.188 70.08);
115 | --chart-4: oklch(0.627 0.265 303.9);
116 | --chart-5: oklch(0.645 0.246 16.439);
117 | --sidebar: oklch(0.205 0 0);
118 | --sidebar-foreground: oklch(0.985 0 0);
119 | --sidebar-primary: oklch(0.488 0.243 264.376);
120 | --sidebar-primary-foreground: oklch(0.985 0 0);
121 | --sidebar-accent: oklch(0.269 0 0);
122 | --sidebar-accent-foreground: oklch(0.985 0 0);
123 | --sidebar-border: oklch(1 0 0 / 10%);
124 | --sidebar-ring: oklch(0.556 0 0);
125 | }
126 |
127 | body{
128 | background-image: url("@/assets/mainbackground.svg");
129 | background-size: cover;
130 | background-position: center;
131 | background-repeat: no-repeat;
132 | }
133 |
134 | .centered {
135 | display: flex;
136 | align-items: center;
137 | justify-content: center;
138 | height: 100vh;
139 | }
140 |
141 | input[type=text],
142 | input[type=password] {
143 | padding-top: var(--input-padding);
144 | padding-bottom: var(--input-padding);
145 | padding-left: calc(var(--input-padding) * 2);
146 | padding-right: calc(var(--input-padding) * 2);
147 | color: white;
148 | background-color: black;
149 | border: 2px solid #ccc;
150 | border-radius: var(--input-border-radius);
151 | font-size: 16px;
152 | outline: none;
153 | width: 100%;
154 | transition: border-color 0.3s ease;
155 | }
156 | input[type=text].focus,
157 | input[type=password].focus {
158 | border-color: var(--input-focus-border-color);
159 | }
160 | input::placeholder{
161 | color: #aaa;
162 | }
163 |
164 | .multipart-form {
165 | display: flex;
166 | flex-direction: column;
167 | gap: 15px;
168 | padding: 60px;
169 | width: fit-content;
170 | border: 2px solid darkviolet;
171 | background-color: black;
172 | border-radius: 15px;
173 | align-self: center;
174 | }
175 |
176 | .form-inputs {
177 | display: flex;
178 | flex-direction: column;
179 | align-items: center;
180 | justify-content: center;
181 | gap: 15px;
182 | width: 100%;
183 | }
184 |
185 | .primary-button {
186 | background: linear-gradient(to right, rgba(171, 14, 255, 0.4), rgba(104, 97, 255, 0.4));
187 | font-weight: bold;
188 | align-items: center;
189 | border: none;
190 | color: white;
191 | border-radius: 12px;
192 | padding: 10px 20px;
193 | font-size: 20px;
194 | cursor: pointer;
195 | position: relative;
196 | width: 300px;
197 | height: 50px;
198 | text-align: center;
199 | }
200 |
201 | .bg-pattern{
202 | background-color: #000000;
203 | opacity: 0.8;
204 | background-image: repeating-radial-gradient( circle at 0 0, transparent 0, #000000 10px ), repeating-linear-gradient( #8F297A55, #8F297A );
205 | }
206 |
207 | @layer base{
208 | *{
209 | @apply font-[Inter] m-0 p-0
210 | }
211 | body{
212 | @apply min-h-screen
213 | }
214 | }
215 |
216 | @layer utilities {
217 | .primary-gradient {
218 | @apply bg-gradient-to-r from-[var(--color-primary)] via-[var(--color-secondary)] to-[var(--color-third)];
219 | }
220 | .text-gradient {
221 | @apply bg-gradient-to-r from-[var(--color-primary)] via-[var(--color-secondary)] to-[var(--color-third)] bg-clip-text text-transparent;
222 | }
223 | .link {
224 | @apply text-[var(--color-secondary)] hover:text-gradient underline transition-colors duration-150;
225 | }
226 | .card{
227 | @apply
228 | }
229 | }
230 |
231 | @layer components{
232 | .auth-form{
233 | @apply flex flex-col justify-center
234 | }
235 | .media-header{
236 | @apply text-white text-center text-3xl
237 | }
238 | .dashboard-button{
239 | @apply text-white px-4 py-1 cursor-pointer hover:bg-neutral-700 hover:rounded-full transition-[0.3s]
240 | }
241 | .dashboard-active-button{
242 | @apply text-white px-4 py-1 cursor-pointer bg-neutral-600 hover:bg-neutral-500 rounded-xl transition-[0.3s]
243 | }
244 |
245 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/layout/DashboardLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet, NavLink } from 'react-router-dom';
2 |
3 | const DashboardLayout = () => {
4 | return (
5 | <>
6 |
7 |
10 | isActive ? 'dashboard-active-button' : 'dashboard-button'}
11 | >
12 | Организации
13 |
14 |
17 | isActive ? 'dashboard-active-button' : 'dashboard-button'}
18 | >
19 | Команды
20 |
21 |
24 | isActive ? 'dashboard-active-button' : 'dashboard-button'}
25 | >
26 | Проекты
27 |
28 |
29 |
30 | >
31 | );
32 | };
33 |
34 | export default DashboardLayout;
35 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/main.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.tsx'
5 | import { BrowserRouter } from 'react-router-dom'
6 |
7 | createRoot(document.getElementById('root')!).render(
8 |
9 |
10 |
11 | )
12 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/aboutUs/aboutPage.tsx:
--------------------------------------------------------------------------------
1 | import Card from "@/components/card/Card"
2 |
3 | const AboutPage = () => {
4 | return (
5 |
6 |
7 |
8 | SpeedSolver
9 | Cистема эффективного управления
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Мы используем
17 |
18 |
19 | Backend
20 |
21 | Python
22 | FastAPI
23 | Pydantic
24 | Uvicorn
25 |
26 |
27 |
28 | Frontend
29 |
30 | React
31 | Typescript
32 | Tailwind
33 |
34 |
35 |
36 |
37 |
38 | ORM
39 |
40 | Python SQLAlchemy
41 | Alembic
42 | asyncpg driver
43 |
44 |
45 |
46 | Мониторинг
47 |
48 | Grafana
49 | Prometheus
50 | Node Exporter
51 | AlertManager
52 |
53 |
54 |
55 |
56 |
57 |
База денных: PostgreSQL
58 |
Авторизация: JWT / pyjwt
59 |
Тестирование: pytest
60 |
61 |
62 |
63 |
Контейнеризация: Docker
64 |
Веб-сервер: Nginx
65 |
CI / CD: Github Actions
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
79 | export default AboutPage
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/access/login/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { authorize } from "@/app/axios_api"
3 | import { toast, ToastContainer } from "react-toastify"
4 | import PasswordInput from "@/components/passwordInput/PasswordInput"
5 | export const LoginPage = () => {
6 |
7 | const [authForm, setAuthForm] = useState(
8 | {
9 | username: "",
10 | password: ""
11 | })
12 |
13 |
14 | const handleAuthorize = () => {
15 | authorize(authForm.username, authForm.password)
16 | .then(() => window.location.href = "/dashboard")
17 | .catch(error => toast.error(error))
18 | }
19 |
20 | return (
21 |
22 |
23 |
Войти
24 |
25 |
{
31 | setAuthForm({...authForm, username: e.target.value})}
32 | }/>
33 |
34 |
35 | Авторизоваться
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/access/register/RegisterPage.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from "react"
2 | import { ToastContainer } from "react-toastify"
3 | import PasswordInput from "@/components/passwordInput/PasswordInput"
4 |
5 | export const RegisterPage: React.FC = () => {
6 | const loginInput = useRef(null);
7 |
8 | const handleRegister = (e: React.MouseEvent) => {
9 | e.preventDefault();
10 | const loginValue = loginInput.current?.value;
11 | console.log("Введённый логин:", loginValue);
12 | };
13 |
14 | return (
15 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/dashboard/Companies.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Companies = () => {
4 | return (
5 | Companies
6 | )
7 | }
8 |
9 | export default Companies
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 | import { DashboardNavigation } from "../../components/dashboardNavigation/dashboardNavigation"
3 |
4 |
5 | export const Dashboard: React.FC = () => {
6 |
7 | useEffect(() => {
8 |
9 | })
10 |
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/dashboard/Projects.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Projects = () => {
4 | return (
5 | Projects
6 | )
7 | }
8 |
9 | export default Projects
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/dashboard/Teams.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Teams = () => {
4 | return (
5 | Teams
6 | )
7 | }
8 |
9 | export default Teams
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/teams/TeamsPage.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .content {
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/teams/TeamsPage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { Team } from "../../types/teams/types"
3 | import { get_all_teams } from "../../app/axios_api"
4 | import { TeamsList } from "../../components/teamslist/TeamsList"
5 | import "./TeamsPage.css"
6 |
7 | // interface TeamProps {
8 | // teams: Team[]
9 | // }
10 |
11 | export const TeamsPage = () => {
12 |
13 | const [teams, setTeams] = useState([])
14 |
15 | useEffect(() => {
16 | get_all_teams().then(res => setTeams(res))
17 | console.log(teams)
18 | }, [])
19 |
20 |
21 | return (
22 |
27 | )
28 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/test/TestPage.tsx:
--------------------------------------------------------------------------------
1 | import PasswordInput from "@/components/passwordInput/PasswordInput"
2 |
3 | const TestPage = () => {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default TestPage
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/welcome/WelcomePage.module.css:
--------------------------------------------------------------------------------
1 |
2 | .container {
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | height: 100vh;
8 | background-image: url("../../assets/mainbackground.svg");
9 | background-size: cover;
10 | background-position: center;
11 | background-repeat: no-repeat;
12 | gap: 90px;
13 | }
14 |
15 | .button_container {
16 | display: flex;
17 | justify-content: center;
18 | gap: 70px;
19 | }
20 |
21 | .title_text {
22 | color: white;
23 | margin-bottom: 300px;
24 | font-size: 70px;
25 | }
26 |
27 | @media (max-width: 320px) {
28 | .button_container {
29 | flex-direction: column;
30 | gap: 30px;
31 | }
32 | }
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/pages/welcome/WelcomePage.tsx:
--------------------------------------------------------------------------------
1 | import 'react-toastify/dist/ReactToastify.css';
2 |
3 | import { ToastContainer } from "react-toastify";
4 | import { Link } from "react-router-dom";
5 | import AnimatedText from '@/components/swappingText/SwappingText';
6 |
7 | const WelcomePage = () => {
8 |
9 | const flexText:string[] = ['создавайте организации.', 'создавайте команды.', 'распределяйте задачи.', 'планируйте сроки.']
10 |
11 | return (
12 |
13 |
14 |
15 |
SpeedSolver
16 |
17 |
18 |
19 |
20 |
21 | Войти
22 |
23 |
24 |
25 | Зарегистрироваться
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default WelcomePage
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/types/teams/types.ts:
--------------------------------------------------------------------------------
1 | // types.ts
2 | export interface Profile {
3 | patronymic: string;
4 | id: string;
5 | about: string;
6 | name: string;
7 | surname: string;
8 | birthdate: string;
9 | }
10 |
11 | export interface Leader {
12 | id: string;
13 | email: string;
14 | registered: string;
15 | password: string;
16 | is_mail_verified: boolean;
17 | profile: Profile;
18 | }
19 |
20 | export interface Team {
21 | id: string;
22 | leaderId: string;
23 | title: string;
24 | description: string;
25 | organizationId: string | null;
26 | leader: Leader;
27 | }
28 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/utils/cookieUtils.ts:
--------------------------------------------------------------------------------
1 | // cookieUtils.ts
2 | export function getCookie(name: string): string | null {
3 | const value = `; ${document.cookie}`;
4 | const parts = value.split(`; ${name}=`);
5 | if (parts.length === 2) {
6 | const cookieValue = parts.pop()!.split(';').shift();
7 | return cookieValue ? cookieValue : null;
8 | }
9 | return null;
10 | }
11 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true,
24 |
25 |
26 | /* Shadcn ui */
27 | "baseUrl": ".",
28 | "paths": {
29 | "@/*": [
30 | "./src/*"
31 | ]
32 | }
33 | },
34 | "include": ["src"]
35 | }
36 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/SpeedSolverFrontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import tailwindcss from '@tailwindcss/vite'
3 | import react from '@vitejs/plugin-react'
4 | import path from 'path';
5 |
6 | // https://vite.dev/config/
7 | export default defineConfig({
8 | plugins: [react(), tailwindcss()],
9 | resolve: {
10 | alias: {
11 | "@": path.resolve(__dirname, "./src"),
12 | },
13 | },
14 | server: {
15 | host: '0.0.0.0',
16 | port: 3001
17 | }
18 | })
19 |
--------------------------------------------------------------------------------