├── .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 | 10 | 17 | 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 | 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 | 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 | 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 |
16 |
17 |

Регистрация

18 |
19 | 25 | 26 |
27 | 34 |
35 | 36 |
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 |
23 |
24 | 25 |
26 |
27 | ) 28 | } -------------------------------------------------------------------------------- /SpeedSolverFrontend/src/pages/test/TestPage.tsx: -------------------------------------------------------------------------------- 1 | import PasswordInput from "@/components/passwordInput/PasswordInput" 2 | 3 | const TestPage = () => { 4 | return ( 5 |
6 | 7 |
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 | --------------------------------------------------------------------------------