├── .dockerignore ├── .env.example ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── README.md ├── __init__.py ├── alembic.ini ├── commands ├── __init__.py └── createuser │ ├── __init__.py │ ├── createadminrepository.py │ └── registeradmindto.py ├── createsuperuser.py ├── docker-compose.yml ├── migrations ├── README ├── __init__.py ├── auto_migrate.py ├── base.py ├── env.py ├── script.py.mako └── versions │ ├── 2024_04_15_1900-7821b2ea5fcf_.py │ ├── 2024_04_16_1208-1d6f8030164b_.py │ ├── 2024_04_16_1231-5a1729370788_.py │ └── 2024_05_05_0704-91b7f3d23403_.py ├── old ├── __init__.py └── src │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── system │ │ └── __init__.py │ └── v1 │ │ ├── __init__.py │ │ ├── auth │ │ ├── __init__.py │ │ └── routes.py │ │ ├── email_controller.py │ │ ├── invitation_controller.py │ │ ├── login_controller.py │ │ ├── meet_controller.py │ │ ├── permission_controller.py │ │ ├── role_controller.py │ │ └── user_controller.py │ ├── app │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── cors_config.py │ │ ├── db_config.py │ │ ├── email_config.py │ │ ├── logging_config.py │ │ ├── pagination_config.py │ │ ├── project_config.py │ │ ├── security_config.py │ │ └── swagger_config.py │ ├── dependencies │ │ ├── __init__.py │ │ ├── repositories.py │ │ └── services.py │ ├── exception_handler.py │ ├── hooks │ │ ├── __init__.py │ │ ├── on_shutdown.py │ │ └── on_startup.py │ ├── middleware.py │ └── routers.py │ ├── domain │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── auth_dto.py │ │ ├── auth_service.py │ │ ├── dto │ │ │ ├── __init__.py │ │ │ └── registration.py │ │ ├── token_dto.py │ │ └── token_service.py │ ├── email │ │ ├── __init__.py │ │ ├── email_dto.py │ │ └── email_service.py │ ├── invitation │ │ ├── __init__.py │ │ ├── invitation_dto.py │ │ ├── invitation_entity.py │ │ └── invitation_service.py │ ├── login │ │ ├── __init__.py │ │ ├── login_dto.py │ │ └── login_service.py │ ├── meet │ │ ├── __init__.py │ │ ├── meet_dto.py │ │ └── meet_service.py │ ├── permission │ │ ├── __init__.py │ │ ├── permission_dto.py │ │ └── permission_service.py │ ├── role │ │ ├── __init__.py │ │ ├── role_dto.py │ │ └── role_service.py │ └── user │ │ ├── __init__.py │ │ ├── user_dto.py │ │ ├── user_entity.py │ │ └── user_service.py │ ├── infra │ ├── __init__.py │ ├── database │ │ ├── __init__.py │ │ ├── db_helper.py │ │ └── session.py │ ├── models │ │ ├── __init__.py │ │ ├── base_model.py │ │ ├── feature_member_model.py │ │ ├── feature_model.py │ │ ├── feature_tag.py │ │ ├── invite_registration.py │ │ ├── permission_model.py │ │ ├── project_member_permission.py │ │ ├── project_model.py │ │ ├── project_user_model.py │ │ ├── role_model.py │ │ ├── tag_model.py │ │ ├── task_member_model.py │ │ ├── task_model.py │ │ ├── task_tag.py │ │ ├── user_model.py │ │ └── user_permission_model.py │ ├── repositories │ │ ├── __init__.py │ │ ├── base_repository.py │ │ ├── email_repository.py │ │ ├── generic_alchemy_repo.py │ │ ├── invitation_repository.py │ │ ├── login_repository.py │ │ ├── permission_repository.py │ │ ├── role_repository.py │ │ └── user_repository.py │ └── services │ │ ├── __init__.py │ │ └── email_service.py │ └── main.py ├── poetry.lock ├── pyproject.toml ├── src ├── __init__.py ├── api │ └── v1 │ │ ├── auth │ │ ├── controller.py │ │ └── dtos │ │ │ ├── login.py │ │ │ ├── registration.py │ │ │ └── token.py │ │ ├── permission │ │ └── routes.py │ │ ├── project │ │ └── routes.py │ │ ├── routes.py │ │ └── user │ │ └── controller.py ├── app.py ├── apps │ ├── __init__.py │ ├── auth │ │ ├── dependends │ │ │ ├── service.py │ │ │ └── token_service.py │ │ ├── exceptions │ │ │ └── token.py │ │ ├── service.py │ │ └── token_service.py │ ├── email │ │ ├── dependends.py │ │ └── service.py │ ├── invitation │ │ ├── __init__.py │ │ ├── dto.py │ │ ├── exceptions.py │ │ ├── models.py │ │ ├── repository.py │ │ └── service.py │ ├── permissions │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── permission.py │ │ │ ├── project.py │ │ │ └── user.py │ ├── project │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── feature.py │ │ │ ├── feature_member.py │ │ │ ├── project.py │ │ │ ├── project_member.py │ │ │ ├── task.py │ │ │ └── task_member.py │ ├── tags │ │ ├── __init__.py │ │ └── models │ │ │ ├── __init__.py │ │ │ ├── feature.py │ │ │ ├── tag.py │ │ │ └── task.py │ └── user │ │ ├── __init__.py │ │ ├── depenends │ │ ├── repository.py │ │ └── service.py │ │ ├── dto.py │ │ ├── entity.py │ │ ├── models │ │ ├── __init__.py │ │ ├── role.py │ │ └── user.py │ │ ├── repositories │ │ └── user.py │ │ └── service.py ├── config │ ├── cors.py │ ├── database │ │ ├── engine.py │ │ ├── session.py │ │ └── settings.py │ ├── email.py │ ├── jwt_config.py │ ├── logging.py │ ├── project.py │ ├── security.py │ └── swagger.py ├── lib │ ├── __init__.py │ ├── base_model.py │ ├── dtos │ │ ├── __init__.py │ │ └── base_dto.py │ └── exceptions.py ├── main.py ├── middleware.py └── routes.py └── start.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .git 3 | .gitignore 4 | .env 5 | docker*.yml 6 | .gitlab-ci.yml 7 | .pre-commit-config.yaml 8 | 9 | # IDE 10 | .idea 11 | .vscode 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ===== SYSTEM VARIABLES ===== 2 | PYTHONDONTWRITEBYTECODE=1 3 | PYTHONBUFFERED=1 4 | 5 | # ===== LOGGING ===== 6 | LOGGING_ON=True 7 | LOGGING_LEVEL=DEBUG 8 | LOGGING_JSON=True 9 | 10 | # ===== MAIN ===== 11 | APP_DEBUG=True 12 | APP_HOST=0.0.0.0 13 | APP_PORT=8000 14 | 15 | # ===== SWAGGER ===== 16 | APP_DOCS_URL=/docs 17 | APP_REDOC_URL=/docs/redoc 18 | APP_TITLE="СУП" 19 | APP_DESCRIPTION=../README.md 20 | APP_VERSION=0.2.0 21 | 22 | # ===== SECURITY ===== 23 | ACCESS_TOKEN_EXPIRE_MINUTES=60 24 | SECRET_KEY=HGDIYGS7gsguIGS*g&(SGcg&(*CGS*&C*SGC&*GCGCuo9shGpisudp9sU 25 | 26 | # ===== CORS ===== 27 | CORS_ALLOW_ORIGINS="http://localhost:3000" 28 | #CORS_ALLOW_METHODS= 29 | #CORS_ALLOW_HEADERS= 30 | #CORS_ALLOW_CREDENTIALS= 31 | #CORS_ALLOW_ORIGIN_REGEX= 32 | #CORS_EXPOSE_HEADERS= 33 | CORS_MAX_AGE=600 34 | 35 | # ===== POSTGRES ===== 36 | POSTGRES_USER=postgres 37 | POSTGRES_PASSWORD=postgres 38 | POSTGRES_DB=sup 39 | POSTGRES_HOST=db 40 | POSTGRES_PORT=5432 41 | 42 | # ===== DATABASE ===== 43 | DB_URL_SCHEME=postgresql+asyncpg 44 | DB_HOST=db 45 | DB_PORT=5432 46 | DB_PASSWORD=postgres 47 | DB_NAME=sup 48 | DB_USER=postgres 49 | DB_ECHO_LOG=True 50 | DB_RUN_AUTO_MIGRATE=True 51 | 52 | # ==== SMTP ==== # 53 | SMTP_SERVER=* 54 | SMTP_PORT=* 55 | SMTP_USER=* 56 | SMTP_PASSWORD=* 57 | 58 | 59 | # ===== TOKENS ===== 60 | ACCESS_TOKEN_LIFETIME=300 61 | REFRESH_TOKEN_LIFETIME=86400 62 | REFRESH_TOKEN_ROTATE_MIN_LIFETIME=3600 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | idea 3 | .DS_Store 4 | .git 5 | *.egg-info/ 6 | .installed.cfg 7 | *.egg 8 | 9 | dist 10 | db.sqlite3 11 | 12 | venv 13 | .python-version 14 | .env.dev 15 | .env 16 | 17 | .ruff_cache 18 | __pycache__/ 19 | */__pycache__/ 20 | *.py[cod] 21 | .pyc 22 | *.pyc 23 | 24 | !*media/.gitkeep 25 | media/* 26 | 27 | !*static/.gitkeep 28 | static/* 29 | 30 | log/debug.log 31 | 32 | build 33 | doc 34 | home.rst 35 | make.bat 36 | # Makefile 37 | 38 | doc-dev 39 | .vscode/* 40 | /docker-compose.local.yml 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.1.14 4 | hooks: 5 | - id: ruff 6 | args: [--fix] 7 | - id: ruff-format 8 | 9 | # check only py files 10 | files: \.py$ 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.1 as requirements-stage 2 | 3 | WORKDIR /tmp 4 | 5 | RUN pip install poetry 6 | 7 | COPY ./pyproject.toml ./poetry.lock* /tmp/ 8 | 9 | RUN poetry export -f requirements.txt --output requirements.txt --without-hashes 10 | 11 | FROM python:3.12.1 12 | 13 | WORKDIR /app 14 | 15 | COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt 16 | 17 | RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt 18 | 19 | COPY . /app 20 | 21 | COPY start.sh /app/ 22 | 23 | RUN chmod +x /app/start.sh 24 | 25 | CMD ["sh", "/app/start.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Система управления проектами 2 | Платформа должна помочь вести проекты которые реализуются в рамках стажировки молодых специалистов. 3 | 4 | Аналитика успеваемости стажёров. 5 | 6 | Собственная система ведения задач. По причине блокировок пользователей такими ресурсами как Trello и ограничение на использование на подобных ресурсах. 7 | 8 | Управление командами и участниками стажировки. 9 | 10 | ## Старт 11 | переименовать 12 | .env.example на .env 13 | 14 | ### Запустить сборку 15 | ``` 16 | docker-compose up --build 17 | ``` 18 | 19 | ### Alembic migrate 20 | Не выключая контейнеры выполнить команду 21 | ``` 22 | docker exec -it sup-back alembic upgrade head 23 | ``` 24 | 25 | ### Перейти по адресу 26 | ``` 27 | http:\\127.0.0.1:8000\docs 28 | ``` 29 | 30 | ## Alembic создание migrations 31 | Не выключая контейнеры выполнить команду 32 | ``` 33 | docker exec -it sup-back alembic revision --autogenerate -m 'название модели или миграции' 34 | ``` 35 | 36 | 37 | #### Будет полезным для ознакомления (видео) 38 | [Слоистая архитектура](https://youtu.be/aF5_niKPL6c?si=sEqSQYFqU9kPsVDp) 39 | 40 | [Слоистая Архитектура на FastAPI](https://youtu.be/8Im74b55vFc?si=0-ZwffTNCdT6VZAx) 41 | 42 | ## Инструменты 43 | - Python 3.12 44 | - FastAPI 45 | - SqlAlchemy 46 | - Postgres 47 | - Alembic 48 | - Docker 49 | 50 | 51 | ## Архитектура и структура 52 | 53 | ### Контексты 54 | Отчасти подобие элемента DDD (Domain-driven design) - ограниченный контекст (bounded context). 55 | 56 | Не требуется глубоко разбираться в [DDD](https://habr.com/ru/companies/oleg-bunin/articles/551428/), 57 | но без понимания контекста никуда. 58 | 59 | ### Контроллеры 60 | Контроллеры везде называют по разному: `routers`, `endpoints`, `controllers`, мы используем 61 | `controllers`. 62 | 63 | Контроллеры отвечают за запрос\ответ. 64 | Можно сказать что они должны быть "тупые", не иметь бизнес-логики, вообще не иметь бизнес-логики. 65 | 66 | Они вызывают нужные зависимости: авторизация, проверка прав и т.д. 67 | Передают данные в сервис. Обрабатывают `HttpExceptions`. 68 | 69 | ### Сервисы 70 | Сервис - это "бизнес контроллеры", отвечает за бизнес логику, взаимодействуют с 71 | репозиторием и другими сервисами. 72 | 73 | Если в пределах одной сессии (в нашем случае SqlAlchemy) нужно взаимодействовать с 74 | несколькими репозиториями, то следует использовать QueryRepository. 75 | 76 | ### Репозитории 77 | Репозиторий — это коллекция, которая содержит сущности, может фильтровать и возвращать 78 | результат обратно, в зависимости от требований нашего приложения. Где и как он хранит 79 | эти объекты, является ДЕТАЛЬЮ РЕАЛИЗАЦИИ. 80 | 81 | В нашем случае репозиторий может взаимодействовать с базой данных, например Postgres, Redis, 82 | MongoDB. 83 | 84 | Каждый отдельный репозиторий взаимодействует только с одной моделью (таблицей). 85 | 86 | Паттерн репозиторий служит цели отделить логику работы с БД от бизнес-логики приложения. 87 | Лично для себя выделяю основной плюс в переиспользовании методов выборки. 88 | 89 | Примерами методов репозитория могут быть такие названия методов как: 90 | - get_single() 91 | - get_by_id() 92 | - get_user_list() 93 | - get_multi() 94 | - и т.д. 95 | 96 | Также репозиторий может использоваться для create/update/delete операций. 97 | 98 | ## Структура 99 | Контексты - отдельная часть логики проекта. 100 | Содержит свои контроллеры, сервисы, репозитории, зависимости, модели, exceptions и т.д. 101 | 102 | 103 | ### Способ организации кода 104 | - Файлы находятся в папке разбитые по контекстам. 105 | Каждый контекст относится к определенной сущности либо бизнес-процессу. 106 | - Для выборок данных используем паттерн репозиторий. 107 | - Бизнес-логика и операции создания/изменения моделей выносим в сервис-классы. 108 | Сервис классы не хранят свое состояние, что позволяет их переиспользовать без повторной 109 | инициализации. 110 | - Для того чтобы не зависеть от Request в сервисы передаем либо одиночные параметры, 111 | либо DTO (Pydantic Model). 112 | Это позволяет переиспользовать код вне контроллеров (например, команда создания нового 113 | пользователя и т.д.). 114 | - Стараемся, чтобы модели оставались максимально тонкими. В основном содержат в себе связи 115 | (relations). 116 | - Все relation_ship lazy должны быть raise_on_sql 117 | 118 | ### Директории и файлы 119 | #### Основное 120 | - migrations - директория alembic для миграций 121 | - migrations/versions - файлы миграций 122 | - migrations/base.py - файл с импортированными модулями моделей для работы автогенерации миграций 123 | - migrations/env.py - скрипт alembic для работы миграций 124 | - 125 | - src - верхний уровень, содержит общие routes, main.py и все контексты 126 | - src/config - директория для общих настроек 127 | - src/config/database/db_config.py - настройки базы данных 128 | - src/config/database/db_helper.py - получение сессии базы данных 129 | - src/config/project_config.py - настройки для проекта 130 | - src/main.py - корень проекта, который запускает приложение FastAPI 131 | - src/routes.py - общие routers для всех приложений проекта 132 | - 133 | - tests - тесты проекта 134 | - .env.example - пример (шаблон) для файла .env, переменные окружения 135 | - pyproject.toml - файл зависимостей для [poetry](https://python-poetry.org/docs/) 136 | - poetry.lock - обеспечить согласованность между текущими установленными зависимостями и 137 | теми, которые вы указали в файле pyproject.toml 138 | 139 | #### Контексты 140 | 141 | 142 | #### Библиотека (переиспользуемый код) 143 | - src/lib/models/base_model - базовый класс SqlAlchemy 144 | - src/lib/dtos/base_dto - класс базовой модели Pydantic, с настройкой для интеграция с ORM (Ранее известный 145 | как "ORM Mode"/from_orm) 146 | 147 | ### Файлы контекста 148 | - prefix_controller.py - контроллеры контекста 149 | - prefix_repository.py - работа с БД (Postgres, Redis, MongoDB и т.д.) 150 | - prefix_service.py - специфичная для модуля бизнес-логика 151 | - prefix_schema.py - pydantic модели 152 | - routes.py - общие routes для всех контроллеров контекста 153 | - prefix_model.py - модели ORM 154 | 155 | ### Дополнительные файлы контекста 156 | - dependencies.py - зависимости для контекста 157 | - exceptions.py - специфические для контекста исключения 158 | - constants.py - константы контекста 159 | 160 | ## Соглашения 161 | 162 | - Логическая часть проекта находиться внутри контекста 163 | - Контекст называем в единственном числе (например session, user, support) 164 | - Избегаем длинных названий файлов и используем `_` (например user_service.py) 165 | - Сущности более одной собираются в папки (например services, schemas) 166 | - Поддерживаем цепочку Controller -> Service -> Repository 167 | - Запросы контекста пишем в локальном репозитории (например support_repository.py) 168 | 169 | ## Импорты 170 | Вначале, описываем библиотеки, группируем логически. 171 | 172 | ``` 173 | from starlette.status import HTTP_400_BAD_REQUEST, HTTP_204_NO_CONTENT 174 | from fastapi import Depends, HTTPException, APIRouter 175 | 176 | 177 | ``` 178 | 179 | Перечисляем используемые контексты. 180 | ``` 181 | from ...auth.user_schema import UserSchema 182 | from ...auth.auth_service import is_admin 183 | ``` 184 | 185 | В конце, локальные зависимости контекста. 186 | 187 | ``` 188 | from ..schemas.support_schema import ( 189 | CreateSupportSchema, 190 | UpdateSupportSchema, 191 | SupportResponseSchema 192 | ) 193 | from ..services.support_service import SupportService 194 | ``` 195 | 196 | ## Что дополнительно можно почитать 197 | [DTO в Python. Способы реализации](https://habr.com/ru/articles/752936/) 198 | 199 | [Python и чистая архитектура](https://habr.com/ru/companies/piter/articles/588669/) 200 | 201 | [Архитектура ПО](https://backendinterview.ru/architecture/index.html) 202 | 203 | [Bounded contexts будь проще](https://youtu.be/r_HYgERfMos?si=ZbcPAzIaFzGpkB_D) 204 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/__init__.py -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 10 | 11 | # sys.path path, will be prepended to sys.path if present. 12 | # defaults to the current working directory. 13 | prepend_sys_path = . 14 | 15 | # timezone to use when rendering the date within the migration file 16 | # as well as the filename. 17 | # If specified, requires the python-dateutil library that can be 18 | # installed by adding `alembic[tz]` to the pip requirements 19 | # string value is passed to dateutil.tz.gettz() 20 | # leave blank for localtime 21 | # timezone = 22 | 23 | # max length of characters to apply to the 24 | # "slug" field 25 | # truncate_slug_length = 40 26 | 27 | # set to 'true' to run the environment during 28 | # the 'revision' command, regardless of autogenerate 29 | # revision_environment = false 30 | 31 | # set to 'true' to allow .pyc and .pyo files without 32 | # a source .py file to be detected as revisions in the 33 | # versions/ directory 34 | # sourceless = false 35 | 36 | # version location specification; This defaults 37 | # to alembic/versions. When using multiple version 38 | # directories, initial revisions must be specified with --version-path. 39 | # The path separator used here should be the separator specified by "version_path_separator" below. 40 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 41 | 42 | # version path separator; As mentioned above, this is the character used to split 43 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 44 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 45 | # Valid values for version_path_separator are: 46 | # 47 | # version_path_separator = : 48 | # version_path_separator = ; 49 | # version_path_separator = space 50 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 51 | 52 | # set to 'true' to search source files recursively 53 | # in each "version_locations" directory 54 | # new in Alembic version 1.10 55 | # recursive_version_locations = false 56 | 57 | # the output encoding used when revision files 58 | # are written from script.py.mako 59 | # output_encoding = utf-8 60 | 61 | sqlalchemy.url = postgresql+asyncpg://%(POSTGRES_USER)s:%(POSTGRES_PASSWORD)s@%(POSTGRES_HOST)s:%(POSTGRES_PORT)s/%(POSTGRES_DB)s?async_fallback=True 62 | 63 | 64 | [post_write_hooks] 65 | # post_write_hooks defines scripts or Python functions that are run 66 | # on newly generated revision scripts. See the documentation for further 67 | # detail and examples 68 | 69 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 70 | ; hooks = black 71 | ; black.type = console_scripts 72 | ; black.entrypoint = black 73 | ; black.options = -l 89 REVISION_SCRIPT_FILENAME 74 | 75 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary 76 | # hooks = ruff 77 | # ruff.type = exec 78 | # ruff.executable = %(here)s/.venv/bin/ruff 79 | # ruff.options = --fix REVISION_SCRIPT_FILENAME 80 | 81 | # Logging configuration 82 | [loggers] 83 | keys = root,sqlalchemy,alembic 84 | 85 | [handlers] 86 | keys = console 87 | 88 | [formatters] 89 | keys = generic 90 | 91 | [logger_root] 92 | level = WARN 93 | handlers = console 94 | qualname = 95 | 96 | [logger_sqlalchemy] 97 | level = WARN 98 | handlers = 99 | qualname = sqlalchemy.engine 100 | 101 | [logger_alembic] 102 | level = INFO 103 | handlers = 104 | qualname = alembic 105 | 106 | [handler_console] 107 | class = StreamHandler 108 | args = (sys.stderr,) 109 | level = NOTSET 110 | formatter = generic 111 | 112 | [formatter_generic] 113 | format = %(levelname)-5.5s [%(name)s] %(message)s 114 | datefmt = %H:%M:%S 115 | -------------------------------------------------------------------------------- /commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/commands/__init__.py -------------------------------------------------------------------------------- /commands/createuser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/commands/createuser/__init__.py -------------------------------------------------------------------------------- /commands/createuser/createadminrepository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | 3 | from src.apps.user.entity import UserEntity 4 | from src.apps.user.models.user import UserModel 5 | from src.config.database.session import db_helper 6 | from src.lib.exceptions import AlreadyExistError 7 | 8 | 9 | async def create_user(user_data: UserEntity): 10 | async with db_helper.get_db_session() as session: 11 | user = UserModel(**user_data.__dict__) 12 | session.add(user) 13 | try: 14 | await session.commit() 15 | print(f"{user.name} {user.email} successfully created") 16 | except IntegrityError: 17 | raise AlreadyExistError(f"{user.email} is already exist") 18 | -------------------------------------------------------------------------------- /commands/createuser/registeradmindto.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pydantic import BaseModel, EmailStr, constr, model_validator 4 | 5 | 6 | class CLICreateAdminDTO(BaseModel): 7 | name: constr(max_length=20) 8 | surname: constr(max_length=20) 9 | email: EmailStr 10 | password: constr(min_length=8) 11 | name_telegram: constr(max_length=50) 12 | nick_telegram: constr(max_length=50) 13 | nick_google_meet: constr(max_length=50) 14 | nick_gitlab: constr(max_length=50) 15 | nick_github: constr(max_length=50) 16 | 17 | @model_validator(mode="after") 18 | def code_validate(self): 19 | reg = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!#%*?&]{6,20}$" 20 | pat = re.compile(reg) 21 | mat = re.search(pat, self.password) 22 | if not mat: 23 | raise ValueError( 24 | "password must contain minimum 8 characters, at least one capital letter, number and " 25 | "special character" 26 | ) 27 | return self 28 | -------------------------------------------------------------------------------- /createsuperuser.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from typer import Option, Typer 4 | 5 | from commands.createuser.createadminrepository import create_user 6 | from commands.createuser.registeradmindto import CLICreateAdminDTO 7 | from src.apps.user.entity import UserEntity 8 | 9 | 10 | app = Typer() 11 | 12 | 13 | @app.command() 14 | def hello(name: str): 15 | print(f"Hello {name}") 16 | 17 | 18 | @app.command(help="Create a new admin user") 19 | def createsuperuser( 20 | name: str = Option(default="Roma", help="Имя пользователя"), 21 | surname: str = Option(default="Zhopa", help="Фамилия пользователя"), 22 | email: str = Option(..., help="Email адрес пользователя"), 23 | password: str = Option(..., prompt="Пароль пользователя", hide_input=True), 24 | name_telegram: str = Option(default="@Romazhopa", help="Имя пользователя в Telegram"), 25 | nick_telegram: str = Option(default="Andrey228", help="Никнейм пользователя в Telegram"), 26 | nick_google_meet: str = Option(default="kotenokV1", help="Никнейм пользователя в Google Meet"), 27 | nick_gitlab: str = Option(default="GitLabAdmin", help="Никнейм пользователя в GitLab"), 28 | nick_github: str = Option(default="GitHubAdmin", help="Никнейм пользователя в GitHub"), 29 | ): 30 | dto = CLICreateAdminDTO( 31 | name=name, 32 | surname=surname, 33 | email=email, 34 | password=password, 35 | name_telegram=name_telegram, 36 | nick_telegram=nick_telegram, 37 | nick_google_meet=nick_google_meet, 38 | nick_gitlab=nick_gitlab, 39 | nick_github=nick_github, 40 | ) 41 | registration_data = dto.model_dump() 42 | registration_data["is_admin"] = True 43 | user_entity = UserEntity(**registration_data) 44 | 45 | asyncio.run(create_user(user_data=user_entity)) 46 | 47 | 48 | if __name__ == "__main__": 49 | app() 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | x-app: &default-app 4 | build: . 5 | restart: always 6 | 7 | x-env: &env 8 | env_file: 9 | - .env 10 | 11 | services: 12 | api: 13 | <<: [*default-app, *env] 14 | container_name: sup-back 15 | ports: 16 | - 8000:8000 17 | volumes: 18 | - .:/app 19 | depends_on: 20 | db: 21 | condition: service_healthy 22 | 23 | db: 24 | environment: 25 | - POSTGRES_DB=${DB_NAME} 26 | - POSTGRES_USER=${DB_USER} 27 | - POSTGRES_PASSWORD=${DB_PASSWORD} 28 | - POSTGRES_HOST=${DB_HOST} 29 | - POSTGRES_PORT=${DB_PORT} 30 | container_name: sup-db 31 | image: postgres:15.3-alpine 32 | ports: 33 | - 5432:5432 34 | healthcheck: 35 | test: pg_isready -d ${DB_NAME} -U ${DB_USER} 36 | interval: 10s 37 | timeout: 5s 38 | retries: 5 39 | volumes: 40 | - sup_pg_data:/var/lib/postgresql/data 41 | 42 | volumes: 43 | sup_pg_data: 44 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /migrations/auto_migrate.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | 5 | def run_alembic_command(): 6 | alembic_command = ["alembic"] 7 | 8 | if len(sys.argv) > 1: 9 | alembic_command.extend(sys.argv[1:]) 10 | else: 11 | alembic_command.extend(["upgrade", "head"]) 12 | 13 | subprocess.run(alembic_command, check=True) 14 | 15 | 16 | if __name__ == "__main__": 17 | run_alembic_command() 18 | -------------------------------------------------------------------------------- /migrations/base.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa F401 2 | from src.apps.user.models import * 3 | from src.apps.project.models import * 4 | from src.apps.tags.models import * 5 | from src.apps.invitation.models import * 6 | from src.apps.permissions.models import * 7 | -------------------------------------------------------------------------------- /migrations/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 | from migrations.base import Base 8 | from src.config.database.settings import settings 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | config.set_main_option("sqlalchemy.url", settings.database_url) 15 | 16 | 17 | # Interpret the config file for Python logging. 18 | # This line sets up loggers basically. 19 | if config.config_file_name is not None: 20 | fileConfig(config.config_file_name) 21 | 22 | # add your model's MetaData object here 23 | # for 'autogenerate' support 24 | # from myapp import mymodel 25 | # target_metadata = mymodel.Base.metadata 26 | target_metadata = Base.metadata 27 | 28 | # other values from the config, defined by the needs of env.py, 29 | # can be acquired: 30 | # my_important_option = config.get_main_option("my_important_option") 31 | # ... etc. 32 | 33 | 34 | def run_migrations_offline() -> None: 35 | """Run migrations in 'offline' mode. 36 | 37 | This configures the context with just a URL 38 | and not an Engine, though an Engine is acceptable 39 | here as well. By skipping the Engine creation 40 | we don't even need a DBAPI to be available. 41 | 42 | Calls to context.execute() here emit the given string to the 43 | script output. 44 | 45 | """ 46 | url = config.get_main_option("sqlalchemy.url") 47 | context.configure( 48 | url=url, 49 | target_metadata=target_metadata, 50 | literal_binds=True, 51 | dialect_opts={"paramstyle": "named"}, 52 | ) 53 | 54 | with context.begin_transaction(): 55 | context.run_migrations() 56 | 57 | 58 | def run_migrations_online() -> None: 59 | """Run migrations in 'online' mode. 60 | 61 | In this scenario we need to create an Engine 62 | and associate a connection with the context. 63 | 64 | """ 65 | connectable = engine_from_config( 66 | config.get_section(config.config_ini_section, {}), 67 | prefix="sqlalchemy.", 68 | poolclass=pool.NullPool, 69 | ) 70 | 71 | with connectable.connect() as connection: 72 | context.configure( 73 | connection=connection, target_metadata=target_metadata 74 | ) 75 | 76 | with context.begin_transaction(): 77 | context.run_migrations() 78 | 79 | 80 | if context.is_offline_mode(): 81 | run_migrations_offline() 82 | else: 83 | run_migrations_online() 84 | -------------------------------------------------------------------------------- /migrations/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 | -------------------------------------------------------------------------------- /migrations/versions/2024_04_15_1900-7821b2ea5fcf_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7821b2ea5fcf 4 | Revises: 5 | Create Date: 2024-04-15 19:00:43.538002 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 = '7821b2ea5fcf' 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 | op.create_table('permissions', 24 | sa.Column('title', sa.String(length=20), nullable=False), 25 | sa.Column('code', sa.Integer(), nullable=False), 26 | sa.Column('description', sa.String(), nullable=True), 27 | sa.Column('id', sa.Integer(), nullable=False), 28 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 29 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 30 | sa.PrimaryKeyConstraint('id'), 31 | sa.UniqueConstraint('code'), 32 | sa.UniqueConstraint('id') 33 | ) 34 | op.create_table('project', 35 | sa.Column('title', sa.String(), nullable=False), 36 | sa.Column('description', sa.String(), nullable=False), 37 | sa.Column('id', sa.Integer(), nullable=False), 38 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 39 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 40 | sa.PrimaryKeyConstraint('id'), 41 | sa.UniqueConstraint('id') 42 | ) 43 | op.create_table('roles', 44 | sa.Column('name', sa.String(), nullable=False), 45 | sa.Column('color', sa.String(), nullable=False), 46 | sa.Column('id', sa.Integer(), nullable=False), 47 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 48 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 49 | sa.PrimaryKeyConstraint('id'), 50 | sa.UniqueConstraint('id') 51 | ) 52 | op.create_table('tag', 53 | sa.Column('title', sa.String(), nullable=False), 54 | sa.Column('id', sa.Integer(), nullable=False), 55 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 56 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 57 | sa.PrimaryKeyConstraint('id'), 58 | sa.UniqueConstraint('id') 59 | ) 60 | op.create_table('feature', 61 | sa.Column('project_id', sa.Integer(), nullable=False), 62 | sa.Column('priority', sa.Enum('low', 'medium', 'high', name='priority', create_constraint=True), nullable=False), 63 | sa.Column('status', sa.Enum('discussion', 'development', 'closed', name='status', create_constraint=True), nullable=False), 64 | sa.Column('title', sa.String(), nullable=False), 65 | sa.Column('description', sa.String(), nullable=False), 66 | sa.Column('id', sa.Integer(), nullable=False), 67 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 68 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 69 | sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'), 70 | sa.PrimaryKeyConstraint('project_id', 'id'), 71 | sa.UniqueConstraint('id') 72 | ) 73 | op.create_table('users', 74 | sa.Column('name', sa.String(length=20), nullable=False), 75 | sa.Column('surname', sa.String(length=20), nullable=True), 76 | sa.Column('email', sa.String(length=50), nullable=False), 77 | sa.Column('password', sa.String(), nullable=False), 78 | sa.Column('avatar_link', sa.String(length=50), nullable=False), 79 | sa.Column('name_telegram', sa.String(length=50), nullable=False), 80 | sa.Column('nick_telegram', sa.String(length=50), nullable=False), 81 | sa.Column('nick_google_meet', sa.String(length=50), nullable=False), 82 | sa.Column('nick_gitlab', sa.String(length=50), nullable=False), 83 | sa.Column('nick_github', sa.String(length=50), nullable=False), 84 | sa.Column('is_active', sa.Boolean(), nullable=False), 85 | sa.Column('is_admin', sa.Boolean(), nullable=False), 86 | sa.Column('role_id', sa.Integer(), nullable=False), 87 | sa.Column('id', sa.Integer(), nullable=False), 88 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 89 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 90 | sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), 91 | sa.PrimaryKeyConstraint('id'), 92 | sa.UniqueConstraint('id') 93 | ) 94 | op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) 95 | op.create_table('feature_tag', 96 | sa.Column('tag_id', sa.Integer(), nullable=False), 97 | sa.Column('feature_id', sa.Integer(), nullable=False), 98 | sa.Column('id', sa.Integer(), nullable=False), 99 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 100 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 101 | sa.ForeignKeyConstraint(['feature_id'], ['feature.id'], ondelete='CASCADE'), 102 | sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ondelete='CASCADE'), 103 | sa.PrimaryKeyConstraint('tag_id', 'feature_id', 'id'), 104 | sa.UniqueConstraint('id') 105 | ) 106 | op.create_table('invite_registation', 107 | sa.Column('title', sa.String(), nullable=False), 108 | sa.Column('finish_at', sa.DateTime(), nullable=False), 109 | sa.Column('code', sa.String(), nullable=False), 110 | sa.Column('is_active', sa.Boolean(), nullable=False), 111 | sa.Column('author_id', sa.Integer(), nullable=False), 112 | sa.Column('id', sa.Integer(), nullable=False), 113 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 114 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 115 | sa.ForeignKeyConstraint(['author_id'], ['users.id'], ondelete='CASCADE'), 116 | sa.PrimaryKeyConstraint('id'), 117 | sa.UniqueConstraint('id') 118 | ) 119 | op.create_table('project_user', 120 | sa.Column('user_id', sa.Integer(), nullable=False), 121 | sa.Column('project_id', sa.Integer(), nullable=False), 122 | sa.Column('is_responsible', sa.Boolean(), nullable=False), 123 | sa.Column('id', sa.Integer(), nullable=False), 124 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 125 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 126 | sa.ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'), 127 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 128 | sa.PrimaryKeyConstraint('user_id', 'project_id', 'id'), 129 | sa.UniqueConstraint('id') 130 | ) 131 | op.create_table('task', 132 | sa.Column('title', sa.String(), nullable=False), 133 | sa.Column('description', sa.String(), nullable=False), 134 | sa.Column('status', sa.Enum('discussion', 'development', 'closed', name='status', create_constraint=True), nullable=False), 135 | sa.Column('feature_id', sa.Integer(), nullable=False), 136 | sa.Column('id', sa.Integer(), nullable=False), 137 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 138 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 139 | sa.ForeignKeyConstraint(['feature_id'], ['feature.id'], ondelete='CASCADE'), 140 | sa.PrimaryKeyConstraint('feature_id', 'id'), 141 | sa.UniqueConstraint('id') 142 | ) 143 | op.create_table('user_permission', 144 | sa.Column('user_id', sa.Integer(), nullable=False), 145 | sa.Column('permission_id', sa.Integer(), nullable=False), 146 | sa.Column('id', sa.Integer(), nullable=False), 147 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 148 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 149 | sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), 150 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 151 | sa.PrimaryKeyConstraint('user_id', 'permission_id', 'id'), 152 | sa.UniqueConstraint('id') 153 | ) 154 | op.create_table('feature_member', 155 | sa.Column('member_id', sa.Integer(), nullable=False), 156 | sa.Column('feature_id', sa.Integer(), nullable=False), 157 | sa.Column('is_responsible', sa.Boolean(), nullable=False), 158 | sa.Column('id', sa.Integer(), nullable=False), 159 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 160 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 161 | sa.ForeignKeyConstraint(['feature_id'], ['feature.id'], ondelete='CASCADE'), 162 | sa.ForeignKeyConstraint(['member_id'], ['project_user.id'], ondelete='CASCADE'), 163 | sa.PrimaryKeyConstraint('member_id', 'feature_id', 'id'), 164 | sa.UniqueConstraint('id') 165 | ) 166 | op.create_table('member_permission', 167 | sa.Column('member_id', sa.Integer(), nullable=False), 168 | sa.Column('permission_id', sa.Integer(), nullable=False), 169 | sa.Column('id', sa.Integer(), nullable=False), 170 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 171 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 172 | sa.ForeignKeyConstraint(['member_id'], ['project_user.id'], ondelete='CASCADE'), 173 | sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), 174 | sa.PrimaryKeyConstraint('member_id', 'permission_id', 'id'), 175 | sa.UniqueConstraint('id') 176 | ) 177 | op.create_table('task_member', 178 | sa.Column('member_id', sa.Integer(), nullable=False), 179 | sa.Column('is_responsible', sa.Boolean(), nullable=False), 180 | sa.Column('task_id', sa.Integer(), nullable=False), 181 | sa.Column('id', sa.Integer(), nullable=False), 182 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 183 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 184 | sa.ForeignKeyConstraint(['member_id'], ['project_user.id'], ondelete='CASCADE'), 185 | sa.ForeignKeyConstraint(['task_id'], ['task.id'], ondelete='CASCADE'), 186 | sa.PrimaryKeyConstraint('member_id', 'task_id', 'id'), 187 | sa.UniqueConstraint('id') 188 | ) 189 | op.create_table('task_tag', 190 | sa.Column('tag_id', sa.Integer(), nullable=False), 191 | sa.Column('task_id', sa.Integer(), nullable=False), 192 | sa.Column('id', sa.Integer(), nullable=False), 193 | sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 194 | sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), 195 | sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ondelete='CASCADE'), 196 | sa.ForeignKeyConstraint(['task_id'], ['task.id'], ondelete='CASCADE'), 197 | sa.PrimaryKeyConstraint('tag_id', 'task_id', 'id'), 198 | sa.UniqueConstraint('id') 199 | ) 200 | # ### end Alembic commands ### 201 | 202 | 203 | def downgrade() -> None: 204 | # ### commands auto generated by Alembic - please adjust! ### 205 | op.drop_table('task_tag') 206 | op.drop_table('task_member') 207 | op.drop_table('member_permission') 208 | op.drop_table('feature_member') 209 | op.drop_table('user_permission') 210 | op.drop_table('task') 211 | op.drop_table('project_user') 212 | op.drop_table('invite_registation') 213 | op.drop_table('feature_tag') 214 | op.drop_index(op.f('ix_users_email'), table_name='users') 215 | op.drop_table('users') 216 | op.drop_table('feature') 217 | op.drop_table('tag') 218 | op.drop_table('roles') 219 | op.drop_table('project') 220 | op.drop_table('permissions') 221 | # ### end Alembic commands ### 222 | -------------------------------------------------------------------------------- /migrations/versions/2024_04_16_1208-1d6f8030164b_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 1d6f8030164b 4 | Revises: 7821b2ea5fcf 5 | Create Date: 2024-04-16 12:08:53.502401 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 = '1d6f8030164b' 16 | down_revision: Union[str, None] = '7821b2ea5fcf' 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.create_unique_constraint(None, 'invite_registation', ['id']) 24 | op.create_unique_constraint(None, 'permissions', ['id']) 25 | op.create_unique_constraint(None, 'project', ['id']) 26 | op.create_unique_constraint(None, 'roles', ['id']) 27 | op.create_unique_constraint(None, 'tag', ['id']) 28 | op.alter_column('users', 'role_id', 29 | existing_type=sa.INTEGER(), 30 | nullable=True) 31 | op.create_unique_constraint(None, 'users', ['id']) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade() -> None: 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_constraint(None, 'users', type_='unique') 38 | op.alter_column('users', 'role_id', 39 | existing_type=sa.INTEGER(), 40 | nullable=False) 41 | op.drop_constraint(None, 'tag', type_='unique') 42 | op.drop_constraint(None, 'roles', type_='unique') 43 | op.drop_constraint(None, 'project', type_='unique') 44 | op.drop_constraint(None, 'permissions', type_='unique') 45 | op.drop_constraint(None, 'invite_registation', type_='unique') 46 | # ### end Alembic commands ### 47 | -------------------------------------------------------------------------------- /migrations/versions/2024_04_16_1231-5a1729370788_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 5a1729370788 4 | Revises: 1d6f8030164b 5 | Create Date: 2024-04-16 12:31:14.681184 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 = '5a1729370788' 16 | down_revision: Union[str, None] = '1d6f8030164b' 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.alter_column('users', 'avatar_link', 24 | existing_type=sa.VARCHAR(length=50), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('users', 'avatar_link', 32 | existing_type=sa.VARCHAR(length=50), 33 | nullable=False) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/2024_05_05_0704-91b7f3d23403_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 91b7f3d23403 4 | Revises: 5a1729370788 5 | Create Date: 2024-05-05 07:04:33.928113 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 = '91b7f3d23403' 16 | down_revision: Union[str, None] = '5a1729370788' 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.create_index(op.f('ix_invite_registation_code'), 'invite_registation', ['code'], unique=True) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade() -> None: 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_index(op.f('ix_invite_registation_code'), table_name='invite_registation') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /old/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/__init__.py -------------------------------------------------------------------------------- /old/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/__init__.py -------------------------------------------------------------------------------- /old/src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .system import system_routes 2 | from .v1 import router as v1_router 3 | 4 | 5 | __all__ = [ 6 | "system_routes", 7 | "v1_router", 8 | ] 9 | -------------------------------------------------------------------------------- /old/src/api/system/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi.routing import APIRouter 2 | 3 | system_routes = APIRouter() 4 | 5 | 6 | @system_routes.get("/healthcheck") 7 | async def healthcheck(): 8 | return {"health": "OK"} 9 | -------------------------------------------------------------------------------- /old/src/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from .auth.routes import router as auth 3 | from .invitation_controller import router as invitation 4 | # Справочниковые контроллеры 5 | 6 | router = APIRouter() 7 | 8 | router.include_router(auth) 9 | router.include_router(invitation) 10 | -------------------------------------------------------------------------------- /old/src/api/v1/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/api/v1/auth/__init__.py -------------------------------------------------------------------------------- /old/src/api/v1/auth/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from old.src.app.dependencies.services import IAuthService, IInvitationService 3 | from old.src.domain.auth.dto.registration import RegistrationDTO 4 | from old.src.domain.invitation.invitation_dto import CreateInviteUserDTO 5 | from src.lib import RegistrationError 6 | from old.src.domain.user.user_dto import UserBaseDTO 7 | 8 | router = APIRouter(prefix="/auth", tags=["auth"]) 9 | 10 | 11 | @router.post("/registration", response_model=UserBaseDTO) 12 | async def registration(dto: RegistrationDTO, service: IAuthService): 13 | """ 14 | controller for registration user 15 | """ 16 | try: 17 | return await service.registration(dto) 18 | except (RegistrationError, ValueError) as e: 19 | raise HTTPException(status_code=400, detail=str(e)) 20 | 21 | 22 | @router.post("/invite", response_model=UserBaseDTO) 23 | async def invite(dto: CreateInviteUserDTO, service: IInvitationService): 24 | """ 25 | controller for registration user 26 | """ 27 | try: 28 | return await service.create(dto) 29 | except (RegistrationError, ValueError) as e: 30 | raise HTTPException(status_code=400, detail=str(e)) -------------------------------------------------------------------------------- /old/src/api/v1/email_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | 3 | from src.app.dependencies.services import IEmailService 4 | from src.domain.email.email_dto import GetEmailCodeDTO 5 | from src.lib.exceptions import InviteError 6 | 7 | 8 | router = APIRouter(prefix="/email", tags=["email"]) 9 | 10 | 11 | @router.get("/", response_model=GetEmailCodeDTO) 12 | async def check_email_code(code: str, service: IEmailService): 13 | try: 14 | return await service.check_code(code) 15 | except InviteError as e: 16 | raise HTTPException(status_code=400, detail=str(e)) 17 | -------------------------------------------------------------------------------- /old/src/api/v1/invitation_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | 3 | from old.src.app.dependencies.services import IInvitationService 4 | from src.lib import InviteError 5 | from old.src.domain.invitation.invitation_dto import ( 6 | InvitationCreateDTO, 7 | GetInvitationListDTO, 8 | InvitationCheckCodeDTO 9 | ) 10 | 11 | 12 | router = APIRouter(prefix="/invitation", tags=["invitation"]) 13 | 14 | 15 | @router.post("/", response_model=InvitationCreateDTO) 16 | async def create_invitation(service: IInvitationService): 17 | return await service.create() 18 | 19 | 20 | @router.get("/", response_model=list[GetInvitationListDTO]) 21 | async def get_list_invitation(service: IInvitationService): 22 | return await service.get_list() 23 | 24 | 25 | @router.get("/", response_model=InvitationCheckCodeDTO) 26 | async def check_code(code: str, service: IInvitationService): 27 | try: 28 | return await service.check(code) 29 | except InviteError as e: 30 | raise HTTPException(status_code=400, detail=str(e)) 31 | -------------------------------------------------------------------------------- /old/src/api/v1/login_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | 3 | from old.src.domain.auth.token_dto import Token 4 | from src.lib import LoginError 5 | from old.src.app.dependencies.services import ILoginService 6 | 7 | router = APIRouter(prefix="/login", tags=["login"]) 8 | 9 | 10 | @router.post("/", response_model=Token) 11 | async def login_user(service: ILoginService, email: str, password: str): 12 | try: 13 | return await service.check(email, password) 14 | except LoginError as e: 15 | raise HTTPException(status_code=400, detail=str(e)) 16 | -------------------------------------------------------------------------------- /old/src/api/v1/meet_controller.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import APIRouter, Depends 4 | from old.src.app.exception_handler import error_handler 5 | from old.src.app.dependencies.services import IMeetService 6 | from old.src.domain.meet.meet_dto import ( 7 | CreateMeetDTO, 8 | MeetDTO, 9 | MeetResponseDTO, 10 | UpdateMeetDTO, 11 | ) 12 | from old.src.domain.meet.meet_service import MeetService 13 | from old.src.infra.database.session import ISession 14 | from old.src import MeetRepository 15 | from old.src import UserMeetRepository 16 | 17 | 18 | router = APIRouter(prefix="/meet", tags=["meet"]) 19 | 20 | 21 | def provide_service(session: ISession): 22 | return MeetService(MeetRepository(session), UserMeetRepository(session)) 23 | 24 | 25 | @router.post("/", response_model=MeetDTO) 26 | @error_handler 27 | async def create_meet(dto: CreateMeetDTO, service: Annotated[MeetService, Depends(provide_service)]): 28 | return await service.create(dto) 29 | 30 | 31 | @router.get("/{pk}", response_model=MeetResponseDTO) 32 | @error_handler 33 | async def get_meet(pk: int, service: Annotated[MeetService, Depends(provide_service)]): 34 | return await service.get(pk) 35 | 36 | 37 | @router.put("/{pk}", response_model=MeetResponseDTO) 38 | @error_handler 39 | async def update_meet(pk: int, dto: UpdateMeetDTO, service: IMeetService): 40 | return await service.update(pk, dto) 41 | 42 | 43 | @router.delete("/{pk}", status_code=204) 44 | @error_handler 45 | async def delete_meet(pk: int, service: IMeetService): 46 | return await service.delete(pk) 47 | -------------------------------------------------------------------------------- /old/src/api/v1/permission_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from old.src.app.dependencies.services import IPermissionService 4 | from old.src.domain.permission.permission_dto import ( 5 | CreatePermissionDTO, 6 | UpdatePermissionDTO, 7 | GetPermissionListDTO 8 | ) 9 | 10 | router = APIRouter(prefix="/permissions", tags=["permissions"]) 11 | 12 | 13 | @router.post("/", response_model=CreatePermissionDTO) 14 | async def create_permission(dto: CreatePermissionDTO, service: IPermissionService): 15 | return await service.create(dto) 16 | 17 | 18 | @router.get("/", response_model=list[GetPermissionListDTO]) 19 | async def get_list_permission(service: IPermissionService, limit: int = 10): 20 | return await service.get_list(limit) 21 | 22 | 23 | @router.put("/{pk}", response_model=UpdatePermissionDTO) 24 | async def update_permission(pk: int, dto: UpdatePermissionDTO, service: IPermissionService): 25 | return await service.update(pk, dto) 26 | 27 | 28 | @router.delete("/{pk}") 29 | async def delete_permission(pk: int, service: IPermissionService): 30 | return await service.delete(pk) 31 | -------------------------------------------------------------------------------- /old/src/api/v1/role_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from old.src.app.dependencies.services import IRoleService 4 | from old.src.domain.role.role_dto import CreateRoleDTO, GetRoleDTO, GetRoleListDTO, UpdateRoleDTO 5 | 6 | 7 | router = APIRouter(prefix="/role", tags=["role"]) 8 | 9 | 10 | @router.post("/", response_model=GetRoleDTO) 11 | async def create_role(dto: CreateRoleDTO, service: IRoleService): 12 | return await service.create(dto) 13 | 14 | 15 | @router.get("/", response_model=list[GetRoleListDTO]) 16 | async def get_list_role(service: IRoleService, limit: int = 10): 17 | return await service.get_list(limit) 18 | 19 | 20 | @router.get("/{pk}", response_model=GetRoleDTO) 21 | async def get_role(pk: int, service: IRoleService): 22 | return await service.get(pk) 23 | 24 | 25 | @router.put("/{pk}", response_model=GetRoleDTO) 26 | async def update_role(pk: int, dto: UpdateRoleDTO, service: IRoleService): 27 | return await service.update(pk, dto) 28 | 29 | 30 | @router.delete("/{pk}") 31 | async def delete_role(pk: int, service: IRoleService): 32 | return await service.delete(pk) 33 | -------------------------------------------------------------------------------- /old/src/api/v1/user_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from src.app.dependencies.services import IUserService 4 | from src.domain.auth.auth_service import CurrentUser 5 | from src.domain.user.user_dto import CreateUserDTO, GetUserDTO, GetUserListDTO, UpdatePasswordDTO, UpdateUserDTO 6 | 7 | 8 | router = APIRouter(prefix="/user", tags=["user"]) 9 | 10 | 11 | @router.post("/", response_model=GetUserDTO) 12 | async def create_user(dto: CreateUserDTO, service: IUserService): 13 | return await service.create(dto) 14 | 15 | 16 | @router.get("/", response_model=list[GetUserListDTO]) 17 | async def get_list_users(current_user: CurrentUser, service: IUserService, limit: int = 50): 18 | if current_user: 19 | return await service.get_list(limit) 20 | return [] 21 | 22 | 23 | @router.get("/{pk}", response_model=GetUserDTO) 24 | async def get_user(pk: int, service: IUserService): 25 | return await service.get(pk) 26 | 27 | 28 | @router.put("/{pk}", response_model=GetUserDTO) 29 | async def update_user(pk: int, dto: UpdateUserDTO, service: IUserService): 30 | return await service.update(pk, dto) 31 | 32 | 33 | @router.delete("/{pk}") 34 | async def delete_message(pk: int, service: IUserService): 35 | return await service.delete(pk) 36 | 37 | 38 | @router.put("/{pk}/change_password", response_model=UpdatePasswordDTO) 39 | async def update_pass(pk: int, dto: UpdatePasswordDTO, service: IUserService): 40 | return await service.update_pass(pk, dto) 41 | -------------------------------------------------------------------------------- /old/src/app/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from contextlib import asynccontextmanager 4 | from pathlib import Path 5 | 6 | from fastapi import FastAPI 7 | 8 | from old.src.app.config.project_config import settings as main_settings 9 | from old.src.app.config.swagger_config import settings as swagger_settings 10 | from old.src.app.config.logging_config import settings as logger_settings, logger_config 11 | 12 | from old.src.app.hooks import on_app_startup, on_app_shutdown 13 | 14 | from old.src.app.middleware import init_middleware 15 | from old.src.app.routers import init_routers 16 | 17 | 18 | @asynccontextmanager 19 | async def lifespan(application: FastAPI): 20 | # BEFORE_STARTUP 21 | if main_settings.hooks_enabled: 22 | await on_app_startup(application) 23 | else: 24 | pass 25 | yield 26 | # AFTER_STARTUP 27 | if main_settings.hooks_enabled: 28 | await on_app_shutdown(application) 29 | else: 30 | pass 31 | 32 | 33 | def get_description(path: Path | str) -> str: 34 | 35 | return path.read_text("UTF-8") if isinstance(path, Path) else Path(path).read_text("UTF-8") 36 | 37 | 38 | def get_application() -> FastAPI: 39 | if logger_settings.logging_on: 40 | logging.config.dictConfig(logger_config) # noqa 41 | application = FastAPI( 42 | title=swagger_settings.title, 43 | # description=get_description(swagger_settings.description), 44 | summary=swagger_settings.summary, 45 | version=main_settings.version, 46 | terms_of_service=swagger_settings.terms_of_service, 47 | contact=swagger_settings.contact, 48 | license_info=swagger_settings.license, 49 | lifespan=lifespan, 50 | root_path=main_settings.root_path, 51 | debug=main_settings.debug, 52 | docs_url=swagger_settings.docs_url if main_settings.debug else None, 53 | redoc_url=swagger_settings.redoc_url if main_settings.debug else None, 54 | openapi_url=f"{swagger_settings.docs_url}/openapi.json" if main_settings.debug else None, 55 | ) 56 | # ---------------MIDDLEWARE--------------- 57 | init_middleware(application) 58 | # ----------------ROUTERS--------------- 59 | init_routers(application) 60 | # ----------------END--------------- 61 | return application 62 | -------------------------------------------------------------------------------- /old/src/app/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/app/config/__init__.py -------------------------------------------------------------------------------- /old/src/app/config/cors_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, List, Tuple, Type 3 | 4 | from pydantic import Field 5 | from pydantic.fields import FieldInfo 6 | from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource 7 | 8 | 9 | class MyCustomSource(EnvSettingsSource): 10 | def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: 11 | if field_name in ["allow_origins", "allow_methods", "allow_headers", "expose_headers"]: 12 | if value: 13 | return value.split(",") 14 | return json.loads(value) if value else value 15 | 16 | 17 | class Settings(BaseSettings): 18 | allow_origins: List[str] = Field(default=["*"], alias="CORS_ALLOW_ORIGINS") 19 | allow_methods: List[str] = Field(default=["GET"], alias="CORS_ALLOW_METHODS") 20 | allow_headers: List[str] = Field(default=["*"], alias="CORS_ALLOW_HEADERS") 21 | allow_credentials: bool = Field(default=False, alias="CORS_ALLOW_CREDENTIALS") 22 | allow_origin_regex: str | None = Field(None, alias="CORS_ALLOW_ORIGIN_REGEX") 23 | expose_headers: List[str] = Field(default=["*"], alias="CORS_EXPOSE_HEADERS") 24 | max_age: int = Field(default=600, alias="CORS_MAX_AGE") 25 | 26 | @classmethod 27 | def settings_customise_sources( 28 | cls, 29 | settings_cls: Type[BaseSettings], 30 | init_settings: PydanticBaseSettingsSource, 31 | env_settings: PydanticBaseSettingsSource, 32 | dotenv_settings: PydanticBaseSettingsSource, 33 | file_secret_settings: PydanticBaseSettingsSource, 34 | ) -> Tuple[PydanticBaseSettingsSource, ...]: 35 | return (MyCustomSource(settings_cls),) 36 | 37 | 38 | settings = Settings() 39 | -------------------------------------------------------------------------------- /old/src/app/config/db_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field, PostgresDsn 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | db_url_scheme: str = Field("postgresql+asyncpg", alias="DB_URL_SCHEME") 7 | # host 8 | db_host: str = Field(..., alias="DB_HOST") 9 | db_port: str = Field(..., alias="DB_PORT") 10 | db_name: str = Field(..., alias="DB_NAME") 11 | # credentials 12 | db_user: str = Field(..., alias="DB_USER") 13 | db_password: str = Field(..., alias="DB_PASSWORD") 14 | # logging 15 | db_echo_log: bool = Field(False, alias="DB_ECHO_LOG") 16 | # run auto-migrate 17 | db_run_auto_migrate: bool = Field(False, alias="DB_RUN_AUTO_MIGRATE") 18 | 19 | @property 20 | def database_url(self) -> PostgresDsn: 21 | """URL для подключения (DSN)""" 22 | return ( 23 | f"{self.db_url_scheme}://{self.db_user}:{self.db_password}@" 24 | f"{self.db_host}:{self.db_port}/{self.db_name}?async_fallback=True" 25 | ) 26 | 27 | 28 | settings = Settings() 29 | -------------------------------------------------------------------------------- /old/src/app/config/email_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | # credentials 7 | email_username: str = Field(..., alias="EMAIL_USERNAME") 8 | email_password: str = Field(..., alias="EMAIL_PASSWORD") 9 | 10 | # server config 11 | 12 | smtp_port: int = Field(465, alias="SMTP_PORT") 13 | smtp_host: str = Field("smtp.gmail.com", alias="SMTP_HOST") 14 | 15 | 16 | settings = Settings() 17 | -------------------------------------------------------------------------------- /old/src/app/config/logging_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | logging_on: bool = Field(default=True, alias="LOGGING_ON") 7 | logging_level: str = Field(default="DEBUG", alias="LOGGING_LEVEL") 8 | logging_json: bool = Field(default=True, alias="LOGGING_JSON") 9 | 10 | @property 11 | def log_config(self) -> dict: 12 | config = { 13 | "loggers": { 14 | "uvicorn": {"handlers": ["default"], "level": self.logging_level, "propagate": False}, 15 | "sqlalchemy": {"handlers": ["default"], "level": self.logging_level, "propagate": False}, 16 | } 17 | } 18 | return config 19 | 20 | 21 | def make_logger_conf(*confs, log_level, json_log): 22 | fmt = "%(asctime)s.%(msecs)03d [%(levelname)s]|[%(name)s]: %(message)s" 23 | datefmt = "%Y-%m-%d %H:%M:%S" 24 | config = { 25 | "version": 1, 26 | "disable_existing_loggers": True, 27 | "formatters": { 28 | "default": { 29 | "format": fmt, 30 | "datefmt": datefmt, 31 | }, 32 | "json": {"format": fmt, "datefmt": datefmt, "class": "pythonjsonlogger.jsonlogger.JsonFormatter"}, 33 | }, 34 | "handlers": { 35 | "default": { 36 | "level": log_level, 37 | "formatter": "json" if json_log else "default", 38 | "class": "logging.StreamHandler", 39 | "stream": "ext://sys.stdout", 40 | }, 41 | }, 42 | "loggers": { 43 | "": {"handlers": ["default"], "level": log_level, "propagate": False}, 44 | }, 45 | } 46 | for conf in confs: 47 | for key in conf.keys(): 48 | config[key].update(conf[key]) 49 | 50 | return config 51 | 52 | 53 | settings = Settings() 54 | logger_config = make_logger_conf(settings.log_config, log_level=settings.logging_level, json_log=settings.logging_json) 55 | -------------------------------------------------------------------------------- /old/src/app/config/pagination_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | max_limit: int = Field(default=10000, alias="PAGINATION_MAX_LIMIT") 7 | 8 | 9 | settings = Settings() 10 | -------------------------------------------------------------------------------- /old/src/app/config/project_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | host: str = Field(alias="APP_HOST") 7 | port: int = Field(alias="APP_PORT") 8 | debug: bool = Field(default=False, alias="APP_DEBUG") 9 | version: str = Field(alias="APP_VERSION") 10 | hooks_enabled: bool = Field(default=True, alias="APP_HOOKS_ENABLED") 11 | root_path: str = Field(default="", alias="APP_ROOT_PATH") 12 | timezone_shift: int = Field(default=3, alias="APP_TIMEZONE_SHIFT") 13 | 14 | 15 | settings = Settings() 16 | -------------------------------------------------------------------------------- /old/src/app/config/security_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | secret_key: str = Field(..., alias="SECRET_KEY") 7 | access_token_expire_minutes: int = Field(..., alias="ACCESS_TOKEN_EXPIRE_MINUTES") 8 | algorithm: str = Field("HS256", alias="SECRET_KEY_ALGORITHM") 9 | 10 | 11 | settings = Settings() 12 | -------------------------------------------------------------------------------- /old/src/app/config/swagger_config.py: -------------------------------------------------------------------------------- 1 | from pydantic import ( 2 | AnyUrl, 3 | EmailStr, 4 | Field, 5 | ) 6 | from pydantic_settings import BaseSettings 7 | 8 | 9 | class Settings(BaseSettings): 10 | title: str = Field(default="СУП", alias="APP_TITLE") 11 | description: str | None = Field(default=None, alias="APP_DESCRIPTION") 12 | summary: str | None = Field(None, alias="APP_SUMMARY") 13 | terms_of_service: str | None = Field(None, alias="APP_TERMS_OF_SERVICE") 14 | licence_name: str = Field("Apache 2.0", alias="APP_LICENSE_NAME") 15 | licence_identifier: str = Field("MIT", alias="APP_LICENSE_IDENTIFIER") 16 | licence_url: AnyUrl | None = Field("https://www.apache.org/licenses/LICENSE-2.0.html", alias="APP_LICENSE_URL") 17 | contact_name: str | None = Field("Michael Omelchenko", alias="APP_CONTACT_NAME") 18 | contact_url: AnyUrl | None = Field("https://t.me/DJWOMS", alias="APP_CONTACT_URL") 19 | contact_email: EmailStr | None = Field("socanime@gmail.com", alias="APP_CONTACT_EMAIL") 20 | docs_url: str | None = Field(None, alias="APP_DOCS_URL") 21 | redoc_url: str | None = Field(None, alias="APP_REDOC_URL") # noqa 22 | 23 | @property 24 | def contact(self) -> dict: 25 | return {"name": self.contact_name, "url": self.contact_url, "email": self.contact_email} 26 | 27 | @property 28 | def license(self) -> dict: 29 | return {"name": self.licence_name, "url": self.licence_url, "identifier": self.licence_identifier} 30 | 31 | 32 | settings = Settings() 33 | -------------------------------------------------------------------------------- /old/src/app/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/app/dependencies/__init__.py -------------------------------------------------------------------------------- /old/src/app/dependencies/repositories.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends 3 | 4 | from old.src.infra.repositories.login_repository import LoginRepository 5 | from old.src.infra.repositories.permission_repository import PermissionRepository 6 | from old.src.infra.repositories.role_repository import RoleRepository 7 | from old.src.infra.repositories.user_repository import UserRepository 8 | 9 | 10 | IUserRepository = Annotated[UserRepository, Depends()] 11 | IRoleRepository = Annotated[RoleRepository, Depends()] 12 | IPermissionRepository = Annotated[PermissionRepository, Depends()] 13 | ILoginRepository = Annotated[LoginRepository, Depends()] 14 | IEmailRepository = [] -------------------------------------------------------------------------------- /old/src/app/dependencies/services.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends 3 | from old.src.domain.auth.auth_service import AuthService 4 | from old.src.domain.invitation.invitation_service import InvitationService 5 | 6 | 7 | IAuthService = Annotated[AuthService, Depends()] 8 | IInvitationService = Annotated[InvitationService, Depends()] 9 | -------------------------------------------------------------------------------- /old/src/app/exception_handler.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from fastapi import HTTPException, status 4 | from src.lib import NotFoundError, AlreadyExistError 5 | 6 | 7 | def error_handler(func): 8 | @wraps(func) 9 | async def decorator(*args, **kwargs): 10 | try: 11 | return await func(*args, **kwargs) 12 | except NotFoundError as e: 13 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"{e}") 14 | except (ValueError, AlreadyExistError) as e: 15 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"{e}") 16 | # except Exception as e: 17 | # raise HTTPException( 18 | # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 19 | # detail=f"Server Error - {e}" 20 | # ) 21 | 22 | return decorator 23 | -------------------------------------------------------------------------------- /old/src/app/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | from .on_startup import on_app_startup 2 | from .on_shutdown import on_app_shutdown 3 | 4 | __all__ = ( 5 | "on_app_startup", 6 | "on_app_shutdown", 7 | ) 8 | -------------------------------------------------------------------------------- /old/src/app/hooks/on_shutdown.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | 4 | async def on_app_shutdown(app: FastAPI): 5 | pass 6 | -------------------------------------------------------------------------------- /old/src/app/hooks/on_startup.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | 4 | async def on_app_startup(app: FastAPI): 5 | pass 6 | -------------------------------------------------------------------------------- /old/src/app/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from old.src.app.config.cors_config import settings as cors_settings 5 | 6 | 7 | def init_middleware(app: FastAPI): 8 | app.add_middleware( 9 | CORSMiddleware, 10 | allow_origins=cors_settings.allow_origins, 11 | allow_credentials=cors_settings.allow_credentials, 12 | allow_methods=cors_settings.allow_methods, 13 | allow_headers=cors_settings.allow_headers, 14 | allow_origin_regex=cors_settings.allow_origin_regex, 15 | max_age=cors_settings.max_age, 16 | ) 17 | -------------------------------------------------------------------------------- /old/src/app/routers.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from old.src.api import ( 3 | system_routes, 4 | v1_router, 5 | ) 6 | 7 | 8 | def init_routers(app: FastAPI): 9 | app.include_router(system_routes, prefix="/system", tags=["system"]) 10 | app.include_router(v1_router, prefix="/v1") 11 | -------------------------------------------------------------------------------- /old/src/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/__init__.py -------------------------------------------------------------------------------- /old/src/domain/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/auth/__init__.py -------------------------------------------------------------------------------- /old/src/domain/auth/auth_dto.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class TokenPayload(BaseModel): 7 | id: Union[int, None] = None 8 | -------------------------------------------------------------------------------- /old/src/domain/auth/auth_service.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | 3 | from typing import Annotated 4 | 5 | from fastapi import HTTPException, status, Depends 6 | from fastapi.security import OAuth2PasswordBearer 7 | 8 | from pydantic import ValidationError 9 | from src.lib import RegistrationError, AlreadyExistError 10 | from old.src.app.config.security_config import settings 11 | from old.src.domain.auth.dto.registration import RegistrationDTO 12 | from old.src.app.dependencies.repositories import IUserRepository 13 | from sqlalchemy.exc import IntegrityError 14 | 15 | from old.src.domain.auth.auth_dto import TokenPayload 16 | from old.src.domain.user.user_dto import GetUserDTO, UserBaseDTO 17 | from old.src.domain.user.user_entity import UserEntity 18 | 19 | # reusable_oauth2 = OAuth2PasswordBearer( 20 | # tokenUrl="/v1/login/access-token" 21 | # ) 22 | # 23 | # # TODO: это бы убрать в депенденсис 24 | # TokenDep = Annotated[str, Depends(reusable_oauth2)] 25 | 26 | 27 | class AuthService: 28 | 29 | def __init__(self, repository: IUserRepository): 30 | self.user_repository = repository 31 | 32 | # async def __call__(self, repository: IUserRepository, token: TokenDep): 33 | # try: 34 | # payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) 35 | # token_data = TokenPayload(**payload) 36 | # except (jwt.PyJWTError, ValidationError): 37 | # raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials") 38 | # user = await repository.get(token_data.id) 39 | # if not user: 40 | # raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") 41 | # if not user.active: 42 | # raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user") 43 | # return user 44 | 45 | async def registration(self, dto: RegistrationDTO): 46 | # TODO получить ссылку для регистрации и проверять, активна ли эта ссылка 47 | ragistration_data = dto.model_dump() 48 | ragistration_data.pop('password2') 49 | user_entity = UserEntity(**ragistration_data) 50 | try: 51 | return await self.user_repository.create(user_entity) 52 | except AlreadyExistError as e: 53 | raise RegistrationError(e) 54 | 55 | async def invite(self, dto): 56 | print(dto) 57 | -------------------------------------------------------------------------------- /old/src/domain/auth/dto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/auth/dto/__init__.py -------------------------------------------------------------------------------- /old/src/domain/auth/dto/registration.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, constr 2 | from pydantic import model_validator 3 | import re 4 | 5 | 6 | class RegistrationDTO(BaseModel): 7 | name: constr(max_length=20) 8 | surname: constr(max_length=20) 9 | email: EmailStr 10 | password: constr(min_length=8) 11 | password2: constr(min_length=8) 12 | name_telegram: constr(max_length=50) 13 | nick_telegram: constr(max_length=50) 14 | nick_google_meet: constr(max_length=50) 15 | nick_gitlab: constr(max_length=50) 16 | nick_github: constr(max_length=50) 17 | 18 | @model_validator(mode="after") 19 | def code_validate(self): 20 | if self.password != self.password2: 21 | raise ValueError("password missmatch") 22 | reg = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!#%*?&]{6,20}$" 23 | pat = re.compile(reg) 24 | mat = re.search(pat, self.password) 25 | if not mat: 26 | raise ValueError('password must contain minimum 8 characters, at least one capital letter, number and ' 27 | 'special character') 28 | return self 29 | -------------------------------------------------------------------------------- /old/src/domain/auth/token_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Token(BaseModel): 5 | access_token: str 6 | token_type: str = "bearer" 7 | -------------------------------------------------------------------------------- /old/src/domain/auth/token_service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Annotated 3 | 4 | import jwt 5 | from fastapi import Depends 6 | 7 | from old.src.app.config.security_config import settings 8 | 9 | 10 | class TokenService: 11 | @staticmethod 12 | def create_access_token(user_id: int, expires_delta: timedelta = None) -> str: 13 | if expires_delta: 14 | expire = datetime.utcnow() + expires_delta 15 | else: 16 | expire = datetime.utcnow() + timedelta( 17 | minutes=settings.access_token_expire_minutes 18 | ) 19 | to_encode = {"exp": expire, "id": user_id} 20 | return jwt.encode( 21 | payload=to_encode, 22 | key=settings.secret_key, 23 | algorithm=settings.algorithm 24 | ) 25 | 26 | 27 | ITokenService = Annotated[TokenService, Depends()] 28 | -------------------------------------------------------------------------------- /old/src/domain/email/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/email/__init__.py -------------------------------------------------------------------------------- /old/src/domain/email/email_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr 2 | 3 | 4 | class VerifyBaseDTO(BaseModel): 5 | code: constr(max_length=20) 6 | user_id: int 7 | 8 | 9 | class CreateEmailCodeDTO(VerifyBaseDTO): 10 | pass 11 | 12 | 13 | class GetEmailCodeDTO(VerifyBaseDTO): 14 | pass 15 | -------------------------------------------------------------------------------- /old/src/domain/email/email_service.py: -------------------------------------------------------------------------------- 1 | from src.app.dependencies.repositories import IEmailRepository, IUserRepository 2 | 3 | 4 | class EmailService: 5 | def __init__(self, repository: IEmailRepository, user_repository: IUserRepository): 6 | self.repository = repository 7 | self.user_repository = user_repository 8 | 9 | async def check_code(self, code: str): 10 | invite = await self.repository.get_code(code) 11 | await self.user_repository.update_active(active=True, pk=invite.user_id) 12 | return invite 13 | -------------------------------------------------------------------------------- /old/src/domain/invitation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/invitation/__init__.py -------------------------------------------------------------------------------- /old/src/domain/invitation/invitation_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | from datetime import date 3 | 4 | 5 | class InvitationBaseDTO(BaseModel): 6 | code: str 7 | at_valid: date 8 | status: str = "active" 9 | 10 | 11 | class CreateInviteUserDTO(BaseModel): 12 | email: EmailStr 13 | 14 | 15 | class GetInvitationListDTO(InvitationBaseDTO): 16 | pass 17 | 18 | 19 | class InvitationCreateDTO(InvitationBaseDTO): 20 | pass 21 | 22 | 23 | class InvitationCheckCodeDTO(InvitationBaseDTO): 24 | pass 25 | -------------------------------------------------------------------------------- /old/src/domain/invitation/invitation_entity.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | from datetime import date, timedelta 4 | 5 | 6 | class InvitationEntity: 7 | DAYS = 7 8 | 9 | def get_invitation_code(self): 10 | return self.generate_code() 11 | 12 | def generate_code(self, length=20): 13 | character_sheet = string.ascii_letters + string.digits 14 | rand_code = "".join(secrets.choice(character_sheet) for i in range(length)) 15 | print(f"https://www.google.ru/registration/{rand_code}") 16 | return rand_code 17 | 18 | def generation_date(self): 19 | dates = date.today() + timedelta(days=self.DAYS) 20 | return dates 21 | -------------------------------------------------------------------------------- /old/src/domain/invitation/invitation_service.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from src.lib import InviteError 3 | from old.src.domain.invitation.invitation_dto import InvitationCreateDTO 4 | from old.src.domain.invitation.invitation_entity import InvitationEntity 5 | 6 | 7 | class InvitationService: 8 | 9 | def __init__(self, repository, email_service): 10 | self.repository = repository 11 | self.email_service = email_service 12 | 13 | async def create(self, dto): 14 | print(dto) 15 | # invite = InvitationEntity() 16 | # code = invite.generate_code() 17 | # date = invite.generation_date() 18 | # dto = InvitationCreateDTO(code=code, at_valid=date) 19 | # return await self.repository.create(dto) 20 | 21 | async def get_list(self): 22 | return await self.repository.get_list() 23 | 24 | async def check(self, code: str): 25 | invite = await self.repository.get(code) 26 | at_date = date.today() 27 | if invite is None: 28 | raise InviteError("Not found") 29 | elif at_date > invite.at_valid: 30 | await self.repository.update("expired", invite.id) 31 | raise InviteError("Invalid date") 32 | elif invite.status == "visited": 33 | raise InviteError("Invalid status") 34 | 35 | invite = await self.repository.update("visited", invite.id) 36 | return invite 37 | -------------------------------------------------------------------------------- /old/src/domain/login/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/login/__init__.py -------------------------------------------------------------------------------- /old/src/domain/login/login_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr 2 | 3 | 4 | class LoginBaseDTO(BaseModel): 5 | name: constr(max_length=50) 6 | password: constr(min_length=8) 7 | 8 | 9 | class CreateLoginDTO(LoginBaseDTO): 10 | pass 11 | -------------------------------------------------------------------------------- /old/src/domain/login/login_service.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from old.src.app.config.project_config import settings 4 | from old.src.app.dependencies.repositories import ILoginRepository 5 | from src.lib import LoginError 6 | 7 | from old.src.domain.auth.token_service import ITokenService 8 | from old.src.domain.auth.token_dto import Token 9 | from old.src.domain.user.user_entity import UserEntity 10 | 11 | 12 | class LoginService: 13 | 14 | def __init__(self, repository: ILoginRepository, token_service: ITokenService) -> None: 15 | self.repository = repository 16 | self.token_service = token_service 17 | 18 | async def check(self, email: str, password: str) -> Token: 19 | user = await self.repository.get(email=email) 20 | password1 = UserEntity.set_password(password) 21 | if user is None: 22 | raise LoginError("invalid or login") 23 | elif user.password != password1: 24 | raise LoginError("invalid or password") 25 | elif not user.active: 26 | raise LoginError("Подтвердите аккаунт через почту") 27 | 28 | access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 29 | access_token = Token(access_token=self.token_service.create_access_token( 30 | user.id, expires_delta=access_token_expires, 31 | ) 32 | ) 33 | return access_token 34 | -------------------------------------------------------------------------------- /old/src/domain/meet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/meet/__init__.py -------------------------------------------------------------------------------- /old/src/domain/meet/meet_dto.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class MeetBaseDTO(BaseModel): 7 | title: str 8 | date: datetime 9 | 10 | 11 | class UserMeetDTO(BaseModel): 12 | user_id: int 13 | color: str = "white" 14 | 15 | 16 | class CreateMeetDTO(MeetBaseDTO): 17 | users: list[UserMeetDTO] 18 | 19 | 20 | class UpdateMeetDTO(CreateMeetDTO): 21 | pass 22 | 23 | 24 | class MeetDTO(MeetBaseDTO): 25 | id: int 26 | 27 | 28 | class UserMeetResponseDTO(BaseModel): 29 | id: int 30 | name: str 31 | name_telegram: str 32 | nick_telegram: str 33 | color: str = "white" 34 | 35 | class Config: 36 | from_attributes = True 37 | 38 | 39 | class MeetResponseDTO(MeetBaseDTO): 40 | id: int 41 | users: list[UserMeetResponseDTO] | None = None 42 | 43 | class Config: 44 | from_attributes = True 45 | -------------------------------------------------------------------------------- /old/src/domain/meet/meet_service.py: -------------------------------------------------------------------------------- 1 | from old.src.app.dependencies.repositories import IMeetRepository, IUserMeetRepository 2 | from old.src.domain.meet.meet_dto import CreateMeetDTO, MeetBaseDTO, UpdateMeetDTO, MeetResponseDTO, MeetDTO 3 | 4 | 5 | class MeetService: 6 | 7 | def __init__(self, repository: IMeetRepository, usermeet_repository: IUserMeetRepository): 8 | self.repository = repository 9 | self.usermeet_repository = usermeet_repository 10 | 11 | async def create(self, dto: CreateMeetDTO) -> MeetDTO: 12 | meet = await self.repository.create(MeetBaseDTO(**dto.model_dump(exclude={"users"}))) 13 | user_meet = await self.usermeet_repository.create(dto.users, meet.id) 14 | meet.users = user_meet 15 | return meet 16 | 17 | async def update(self, pk: int, dto: UpdateMeetDTO) -> MeetResponseDTO: 18 | return await self.repository.update(pk, dto) 19 | 20 | async def delete(self, pk: int) -> None: 21 | return await self.repository.delete(pk) 22 | 23 | async def get(self, pk: int) -> MeetResponseDTO: 24 | meet = await self.repository.get(pk) 25 | user_meet = await self.usermeet_repository.get(pk) 26 | get_meet = MeetResponseDTO(**meet.model_dump()) 27 | get_meet.users = user_meet 28 | return get_meet 29 | 30 | -------------------------------------------------------------------------------- /old/src/domain/permission/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/permission/__init__.py -------------------------------------------------------------------------------- /old/src/domain/permission/permission_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, conint, constr 2 | 3 | 4 | class PermissionBaseDTO(BaseModel): 5 | title: constr(max_length=20) 6 | code: conint(ge=0, le=999999) 7 | description: constr(max_length=300) 8 | 9 | 10 | class GetPermissionListDTO(PermissionBaseDTO): 11 | id: int 12 | 13 | 14 | class CreatePermissionDTO(PermissionBaseDTO): 15 | pass 16 | 17 | 18 | class UpdatePermissionDTO(PermissionBaseDTO): 19 | pass 20 | -------------------------------------------------------------------------------- /old/src/domain/permission/permission_service.py: -------------------------------------------------------------------------------- 1 | from old.src.app.dependencies.repositories import IPermissionRepository 2 | from old.src.domain.permission.permission_dto import ( 3 | CreatePermissionDTO, 4 | UpdatePermissionDTO, 5 | GetPermissionListDTO 6 | ) 7 | 8 | 9 | class PermissionService: 10 | 11 | def __init__(self, repository: IPermissionRepository): 12 | self.repository = repository 13 | 14 | async def create(self, dto: CreatePermissionDTO): 15 | return await self.repository.create(dto) 16 | 17 | async def get_list(self, limit: int) -> GetPermissionListDTO: 18 | return await self.repository.get_list(limit) 19 | 20 | async def update(self, pk: int, dto: UpdatePermissionDTO): 21 | return await self.repository.update(dto, pk) 22 | 23 | async def delete(self, pk: int): 24 | return await self.repository.delete(pk) 25 | -------------------------------------------------------------------------------- /old/src/domain/role/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/role/__init__.py -------------------------------------------------------------------------------- /old/src/domain/role/role_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr 2 | 3 | 4 | class RoleBaseDTO(BaseModel): 5 | name: constr(max_length=20) 6 | color: constr(max_length=6) 7 | 8 | 9 | class CreateRoleDTO(RoleBaseDTO): 10 | pass 11 | 12 | 13 | class GetRoleListDTO(RoleBaseDTO): 14 | id: int 15 | 16 | 17 | class GetRoleDTO(RoleBaseDTO): 18 | id: int 19 | 20 | 21 | class UpdateRoleDTO(RoleBaseDTO): 22 | pass 23 | -------------------------------------------------------------------------------- /old/src/domain/role/role_service.py: -------------------------------------------------------------------------------- 1 | from old.src.app.dependencies.repositories import IRoleRepository 2 | from old.src.domain.role.role_dto import CreateRoleDTO, UpdateRoleDTO 3 | 4 | 5 | class RoleService: 6 | 7 | def __init__(self, repository: IRoleRepository): 8 | self.repository = repository 9 | 10 | async def create(self, dto: CreateRoleDTO): 11 | return await self.repository.create(dto) 12 | 13 | async def get_list(self, limit: int): 14 | return await self.repository.get_list(limit) 15 | 16 | async def get(self, pk: int): 17 | return await self.repository.get(pk) 18 | 19 | async def update(self, pk: int, dto: UpdateRoleDTO): 20 | return await self.repository.update(dto, pk) 21 | 22 | async def delete(self, pk: int): 23 | return await self.repository.delete(pk) 24 | -------------------------------------------------------------------------------- /old/src/domain/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/domain/user/__init__.py -------------------------------------------------------------------------------- /old/src/domain/user/user_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, constr 2 | 3 | 4 | class UserBaseDTO(BaseModel): 5 | name: constr(max_length=20) 6 | surname: constr(max_length=20) 7 | email: EmailStr 8 | name_telegram: constr(max_length=50) 9 | nick_telegram: constr(max_length=50) 10 | nick_google_meet: constr(max_length=50) 11 | nick_gitlab: constr(max_length=50) 12 | nick_github: constr(max_length=50) 13 | role_id: int = None 14 | 15 | 16 | class CreateUserDTO(UserBaseDTO): 17 | pass 18 | 19 | 20 | class GetUserListDTO(UserBaseDTO): 21 | id: int 22 | 23 | 24 | class GetUserDTO(UserBaseDTO): 25 | id: int 26 | 27 | 28 | class UpdateUserDTO(UserBaseDTO): 29 | pass 30 | 31 | 32 | class UpdatePasswordDTO(BaseModel): 33 | password: str 34 | -------------------------------------------------------------------------------- /old/src/domain/user/user_entity.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | from dataclasses import dataclass 4 | 5 | from argon2 import PasswordHasher 6 | from pydantic import EmailStr 7 | 8 | 9 | @dataclass 10 | class UserEntity: 11 | name: str 12 | surname: str 13 | email: EmailStr 14 | name_telegram: str 15 | nick_telegram: str 16 | nick_google_meet: str 17 | nick_gitlab: str 18 | nick_github: str 19 | role_id: int | None = None 20 | password: str | None = None 21 | 22 | def get_new_hash_password(self): 23 | password = self.generate_password() 24 | self.password = self.hash_password(password) 25 | return self 26 | 27 | def __post_init__(self): 28 | password = self.hash_password(self.password) 29 | self.password = password 30 | 31 | @staticmethod 32 | def generate_password(length=20): 33 | character_sheet = string.ascii_letters + string.digits + "!@#$%^&*()_+=-" 34 | rand_pass = "".join(secrets.choice(character_sheet) for i in range(length)) 35 | return rand_pass 36 | 37 | @staticmethod 38 | def hash_password(password: str) -> str: 39 | # TODO salt нужно вынести в настройки 40 | salt = "BD^G$#bIUb9PHBF(G#E$_790G(UB#$9E" 41 | hashed = PasswordHasher().hash(password.encode("utf-8"), salt=salt.encode("utf-8")) 42 | return hashed 43 | 44 | @classmethod 45 | def set_password(cls, password): 46 | return cls.hash_password(password) 47 | 48 | def create_verify_code(self, length=16): 49 | character_sheet = string.ascii_letters + string.digits 50 | return "".join(secrets.choice(character_sheet) for i in range(length)) 51 | -------------------------------------------------------------------------------- /old/src/domain/user/user_service.py: -------------------------------------------------------------------------------- 1 | from old.src.app.dependencies.repositories import IEmailRepository 2 | from old.src.app.dependencies.repositories import IUserRepository 3 | from old.src.domain.user.user_dto import CreateUserDTO, UpdateUserDTO, UpdatePasswordDTO 4 | from old.src.domain.user.user_entity import UserEntity 5 | from old.src.domain.email.email_dto import CreateEmailCodeDTO 6 | 7 | 8 | class UserService: 9 | def __init__( 10 | self, 11 | repository: IUserRepository, 12 | email_repository: IEmailRepository, 13 | ): 14 | self.repository = repository 15 | self.email_repository = email_repository 16 | 17 | async def create(self, dto: CreateUserDTO): 18 | user = UserEntity(**dto.model_dump()) 19 | user = user.get_new_hash_password() 20 | user_verify = user.create_verify_code() 21 | user = await self.repository.create(user) 22 | await self.email_repository.create(CreateEmailCodeDTO(user_id=user.id, code=user_verify)) 23 | return user 24 | 25 | async def get_list(self, limit: int): 26 | return await self.repository.get_list(limit) 27 | 28 | async def get(self, pk: int): 29 | return await self.repository.get(pk) 30 | 31 | async def update(self, pk: int, dto: UpdateUserDTO): 32 | return await self.repository.update(dto, pk) 33 | 34 | async def delete(self, pk: int): 35 | return await self.repository.delete(pk) 36 | 37 | async def update_pass(self, pk: int, dto: UpdatePasswordDTO): 38 | new_password = UserEntity.set_password(dto.password) 39 | return await self.repository.update_pass(new_password, pk) 40 | -------------------------------------------------------------------------------- /old/src/infra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/infra/__init__.py -------------------------------------------------------------------------------- /old/src/infra/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/infra/database/__init__.py -------------------------------------------------------------------------------- /old/src/infra/database/db_helper.py: -------------------------------------------------------------------------------- 1 | from asyncio import current_task 2 | from contextlib import asynccontextmanager 3 | 4 | from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session, async_sessionmaker, create_async_engine 5 | 6 | from old.src.app.config.db_config import settings 7 | 8 | 9 | class DatabaseHelper: 10 | """Класс для работы с базой данных""" 11 | 12 | def __init__(self, url: str, echo: bool = False): 13 | self.engine = create_async_engine(url=url, echo=echo) 14 | 15 | self.session_factory = async_sessionmaker( 16 | bind=self.engine, autoflush=False, autocommit=False, expire_on_commit=False 17 | ) 18 | 19 | def get_scope_session(self): 20 | return async_scoped_session(session_factory=self.session_factory, scopefunc=current_task) 21 | 22 | @asynccontextmanager 23 | async def get_db_session(self): 24 | from sqlalchemy import exc 25 | 26 | session: AsyncSession = self.session_factory() 27 | try: 28 | yield session 29 | except exc.SQLAlchemyError: 30 | await session.rollback() 31 | raise 32 | finally: 33 | await session.close() 34 | 35 | async def get_session(self): 36 | from sqlalchemy import exc 37 | 38 | session: AsyncSession = self.session_factory() 39 | try: 40 | yield session 41 | except exc.SQLAlchemyError: 42 | await session.rollback() 43 | raise 44 | finally: 45 | await session.close() 46 | 47 | 48 | db_helper = DatabaseHelper(settings.database_url, settings.db_echo_log) 49 | -------------------------------------------------------------------------------- /old/src/infra/database/session.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import Depends 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | 6 | from .db_helper import db_helper 7 | 8 | 9 | ISession = Annotated[AsyncSession, Depends(db_helper.get_session)] 10 | -------------------------------------------------------------------------------- /old/src/infra/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/infra/models/__init__.py -------------------------------------------------------------------------------- /old/src/infra/models/base_model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import TIMESTAMP, func 4 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 5 | 6 | 7 | class Base(DeclarativeBase): 8 | """Базовая модель SqlAlchemy""" 9 | __abstract__ = True 10 | 11 | id: Mapped[int] = mapped_column(primary_key=True, unique=True) 12 | created_at: Mapped[datetime] = mapped_column( 13 | TIMESTAMP(timezone=True), 14 | server_default=func.now() 15 | ) 16 | updated_at: Mapped[datetime] = mapped_column( 17 | TIMESTAMP(timezone=True), 18 | server_default=func.now(), 19 | onupdate=func.now() 20 | ) 21 | -------------------------------------------------------------------------------- /old/src/infra/models/feature_member_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import List 4 | 5 | from .base_model import Base 6 | 7 | 8 | class FeatureMemberModel(Base): 9 | """ Модель участника фичи 10 | 11 | :param id: идентификатор 12 | :param member_id: id участника проекта 13 | :param feature_id: id фичи 14 | :param is_responsible: Ответственный ли этот пользователь за реализацию фичи 15 | :param created_at: дата создания 16 | :param updated_at: дата обновления 17 | """ 18 | __tablename__ = "feature_member" 19 | member_id: Mapped[int] = mapped_column(ForeignKey( 20 | "project_user.id", 21 | ondelete="CASCADE", 22 | ), 23 | primary_key=True 24 | ) 25 | feature_id: Mapped[int] = mapped_column(ForeignKey( 26 | "feature.id", 27 | ondelete="CASCADE", 28 | ), 29 | primary_key=True 30 | ) 31 | is_responsible: Mapped[bool] = mapped_column( 32 | default=False, 33 | nullable=False 34 | ) 35 | -------------------------------------------------------------------------------- /old/src/infra/models/feature_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey, Enum 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import Literal, get_args, List 4 | 5 | 6 | from .base_model import Base 7 | 8 | 9 | priority_status = Literal["low", "medium", "high"] 10 | activity_status = Literal["discussion", "development", "closed"] 11 | 12 | 13 | class FeatureModel(Base): 14 | """ Модель фич проектов 15 | 16 | :param id: идентификатор 17 | :param user_id: id пользователя 18 | :param permission_id: id разрешения 19 | :param created_at: дата создания 20 | :param updated_at: дата обновления 21 | """ 22 | __tablename__ = "feature" 23 | project_id: Mapped[int] = mapped_column(ForeignKey( 24 | "project.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | priority: Mapped[priority_status] = mapped_column(Enum( 30 | *get_args(priority_status), 31 | name="priority", 32 | create_constraint=True, 33 | validate_strings=True, 34 | )) 35 | status: Mapped[activity_status] = mapped_column(Enum( 36 | *get_args(activity_status), 37 | name="status", 38 | create_constraint=True, 39 | validate_strings=True, 40 | )) 41 | title: Mapped[str] 42 | description: Mapped[str] 43 | members: Mapped[List["ProjectUserModel"]] = relationship( 44 | back_populates="members", 45 | secondary="task_member", 46 | lazy="raise_on_sql" 47 | ) 48 | tags: Mapped[List["FeatureTagModel"]] = relationship( 49 | back_populates="tags", 50 | lazy="raise_on_sql" 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /old/src/infra/models/feature_tag.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from .base_model import Base 5 | 6 | 7 | class FeatureTagModel(Base): 8 | """ Модель тего фичи 9 | 10 | :param id: идентификатор 11 | :param tag_id: id тега 12 | :param feature_id: id фичи 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "feature_tag" 17 | tag_id: Mapped[int] = mapped_column(ForeignKey( 18 | "tag.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | feature_id: Mapped[int] = mapped_column(ForeignKey( 24 | "feature.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | -------------------------------------------------------------------------------- /old/src/infra/models/invite_registration.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy.orm import Mapped, relationship 4 | 5 | from .base_model import Base 6 | 7 | 8 | class MeetModel(Base): 9 | """Модель митапа 10 | 11 | :param id: идентификатор 12 | :param title: название митапа 13 | :param date: дата митапа 14 | :param color: цвет митапа 15 | :param users: пользователи, учавствующие в митапе 16 | :param created_at: дата создания 17 | :param updated_at: дата обновления 18 | """ 19 | 20 | __tablename__ = "meets" 21 | 22 | title: Mapped[str] 23 | date: Mapped[datetime] 24 | users: Mapped[list["UserMeetModel"]] = relationship("UserMeetModel", lazy="raise_on_sql") 25 | -------------------------------------------------------------------------------- /old/src/infra/models/permission_model.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from .base_model import Base 7 | 8 | 9 | class PermissionModel(Base): 10 | """Модель прав доступа 11 | 12 | :param id: идентификатор 13 | :param title: название права доступа 14 | :param code: код права доступа 15 | :param description: описание прав доступа 16 | :param users: все юзеры, у которых есть это право доступа 17 | :param project_members: все участники проекта, у которых есть это право доступа 18 | 19 | """ 20 | __tablename__ = "permissions" 21 | 22 | title: Mapped[str] = mapped_column(String(20)) 23 | code: Mapped[int] = mapped_column(unique=True) 24 | description: Mapped[str] = mapped_column(nullable=True) 25 | users: Mapped[List["UserModel"]] = relationship( 26 | back_populates="permissions", 27 | secondary="user_permission", 28 | lazy="raise_on_sql" 29 | ) 30 | -------------------------------------------------------------------------------- /old/src/infra/models/project_member_permission.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from .base_model import Base 5 | 6 | 7 | class ProjectMemberPermission(Base): 8 | """ Модель разрешений участника проекта 9 | 10 | :param id: идентификатор 11 | :param member_id: id участника проекта 12 | :param permission_id: id разрешения 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "member_permission" 17 | member_id: Mapped[int] = mapped_column(ForeignKey( 18 | "project_user.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | permission_id: Mapped[int] = mapped_column(ForeignKey( 24 | "permissions.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | -------------------------------------------------------------------------------- /old/src/infra/models/project_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship 2 | from typing import List 3 | 4 | from .base_model import Base 5 | 6 | 7 | class ProjectModel(Base): 8 | """Модель проекта 9 | 10 | :param id: идентификатор 11 | :param title: название проекта 12 | :param description: описание проекта 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "project" 17 | 18 | title: Mapped[str] 19 | description: Mapped[str] 20 | users: Mapped[List["UserModel"]] = relationship( 21 | back_populates="project", 22 | secondary="project_user", 23 | lazy="raise_on_sql" 24 | ) 25 | -------------------------------------------------------------------------------- /old/src/infra/models/project_user_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import List 4 | 5 | from .base_model import Base 6 | 7 | 8 | class ProjectUserModel(Base): 9 | """ Модель участника проекта 10 | 11 | :param id: идентификатор 12 | :param user_id: id пользователя 13 | :param project_id: id проекта 14 | :param is_responsible: Ответственный ли этот пользователь за проект 15 | :param permissions: список прав участника 16 | :param created_at: дата создания 17 | :param updated_at: дата обновления 18 | """ 19 | __tablename__ = "project_user" 20 | user_id: Mapped[int] = mapped_column(ForeignKey( 21 | "users.id", 22 | ondelete="CASCADE", 23 | ), 24 | primary_key=True 25 | ) 26 | project_id: Mapped[int] = mapped_column(ForeignKey( 27 | "project.id", 28 | ondelete="CASCADE", 29 | ), 30 | primary_key=True 31 | ) 32 | is_responsible: Mapped[bool] = mapped_column( 33 | default=False, 34 | nullable=False 35 | ) 36 | permissions: Mapped[List["Permission"]] = relationship( 37 | back_populates="permissions", 38 | secondary="member_permission", 39 | lazy="raise_on_sql" 40 | ) 41 | -------------------------------------------------------------------------------- /old/src/infra/models/role_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped 2 | 3 | from .base_model import Base 4 | 5 | 6 | class RoleModel(Base): 7 | """Модель роли 8 | 9 | :param id: идентификатор 10 | :param name: название роли 11 | :param color: цвет роли 12 | :param created_at: дата создания 13 | :param updated_at: дата обновления 14 | """ 15 | __tablename__ = "roles" 16 | 17 | name: Mapped[str] 18 | color: Mapped[str] 19 | -------------------------------------------------------------------------------- /old/src/infra/models/tag_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey, Enum 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import Literal, get_args, List 4 | 5 | 6 | from .base_model import Base 7 | 8 | 9 | class TagModel(Base): 10 | """ Модель фич проектов 11 | 12 | :param id: идентификатор 13 | :param title: название тега 14 | :param created_at: дата создания 15 | :param updated_at: дата обновления 16 | """ 17 | __tablename__ = "tag" 18 | title: Mapped[str] 19 | -------------------------------------------------------------------------------- /old/src/infra/models/task_member_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from .base_model import Base 5 | 6 | 7 | class TaskMemberModel(Base): 8 | """ Модель участника таски 9 | 10 | :param id: идентификатор 11 | :param member_id: id участника проекта 12 | :param task_id: id таски 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "task_member" 17 | member_id: Mapped[int] = mapped_column(ForeignKey( 18 | "project_user.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | is_responsible: Mapped[bool] = mapped_column( 24 | default=False, 25 | nullable=False 26 | ) 27 | task_id: Mapped[int] = mapped_column(ForeignKey( 28 | "task.id", 29 | ondelete="CASCADE", 30 | ), 31 | primary_key=True 32 | ) -------------------------------------------------------------------------------- /old/src/infra/models/task_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship, mapped_column 2 | from sqlalchemy import ForeignKey, Enum 3 | from typing import List, Literal, get_args 4 | 5 | from .base_model import Base 6 | 7 | status = Literal["discussion", "development", "closed"] 8 | 9 | 10 | class TaskModel(Base): 11 | """Модель проекта 12 | 13 | :param id: идентификатор 14 | :param title: название проекта 15 | :param description: описание проекта 16 | :param created_at: дата создания 17 | :param updated_at: дата обновления 18 | """ 19 | __tablename__ = "task" 20 | title: Mapped[str] 21 | description: Mapped[str] 22 | status: Mapped[status] = mapped_column(Enum( 23 | *get_args(status), 24 | name="status", 25 | create_constraint=True, 26 | validate_strings=True, 27 | )) 28 | feature_id: Mapped[int] = mapped_column(ForeignKey( 29 | "feature.id", 30 | ondelete="CASCADE", 31 | ), 32 | primary_key=True 33 | ) 34 | members: Mapped[List["Permission"]] = relationship( 35 | back_populates="permissions", 36 | secondary="member_permission", 37 | lazy="raise_on_sql" 38 | ) 39 | tags: Mapped[List["TaskTagModel"]] = relationship( 40 | back_populates="tags", 41 | lazy="raise_on_sql" 42 | ) 43 | -------------------------------------------------------------------------------- /old/src/infra/models/task_tag.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from .base_model import Base 5 | 6 | 7 | class TaskTagModel(Base): 8 | """ Модель тега для таски 9 | 10 | :param id: идентификатор 11 | :param tag_id: id тега 12 | :param task_id: id таски 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "task_tag" 17 | tag_id: Mapped[int] = mapped_column(ForeignKey( 18 | "tag.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | task_id: Mapped[int] = mapped_column(ForeignKey( 24 | "task.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | -------------------------------------------------------------------------------- /old/src/infra/models/user_model.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import ForeignKey, String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from .base_model import Base 7 | 8 | 9 | class UserModel(Base): 10 | """Модель пользователя 11 | 12 | :param id: идентификатор 13 | :param name: имя пользователя 14 | :param surname: фамилия пользователя 15 | :param email: email пользователя 16 | :param password: пароль пользователя 17 | :param avatar_link: ссылка на аватар пользователя 18 | :param name_telegram: имя в телеграм 19 | :param nick_telegram: ник в телеграм 20 | :param nick_google_meet: ник в google meet 21 | :param nick_gitlab: ник в gitlab 22 | :param nick_github: ник в github 23 | :param is_active: активирован ли пользователь 24 | :param is_admin: является ли пользователь админом 25 | :param role_id: роль пользователя 26 | :param permissions: список прав пользователя 27 | :param created_at: дата создания 28 | :param updated_at: дата обновления 29 | """ 30 | 31 | __tablename__ = "users" 32 | 33 | name: Mapped[str] = mapped_column(String(20)) 34 | surname: Mapped[str] = mapped_column(String(20), nullable=True) 35 | email: Mapped[str] = mapped_column(String(50), unique=True, index=True) 36 | password: Mapped[str] 37 | avatar_link: Mapped[str] = mapped_column(String(50)) 38 | name_telegram: Mapped[str] = mapped_column(String(50)) 39 | nick_telegram: Mapped[str] = mapped_column(String(50)) 40 | nick_google_meet: Mapped[str] = mapped_column(String(50)) 41 | nick_gitlab: Mapped[str] = mapped_column(String(50)) 42 | nick_github: Mapped[str] = mapped_column(String(50)) 43 | is_active: Mapped[bool] = mapped_column(default=False) 44 | is_admin: Mapped[bool] = mapped_column(default=False) 45 | role_id: Mapped[int] = mapped_column(ForeignKey("roles.id")) 46 | permissions: Mapped[List["PermissionModel"]] = relationship( 47 | back_populates="users", secondary="user_permission", lazy="raise_on_sql" 48 | ) 49 | invites: Mapped[List["InviteRegistrationModel"]] = relationship( 50 | back_populates="author" 51 | ) 52 | projects: Mapped[List["InviteRegistrationModel"]] = relationship( 53 | back_populates="user" 54 | ) 55 | -------------------------------------------------------------------------------- /old/src/infra/models/user_permission_model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from .base_model import Base 5 | 6 | 7 | class UserPermissionModel(Base): 8 | """Модель разрешений пользователя 9 | 10 | :param id: идентификатор 11 | :param user_id: id пользователя 12 | :param permission_id: id разрешения 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | 17 | __tablename__ = "user_permission" 18 | user_id: Mapped[int] = mapped_column( 19 | ForeignKey( 20 | "users.id", 21 | ondelete="CASCADE", 22 | ), 23 | primary_key=True, 24 | ) 25 | permission_id: Mapped[int] = mapped_column( 26 | ForeignKey( 27 | "permissions.id", 28 | ondelete="CASCADE", 29 | ), 30 | primary_key=True, 31 | ) 32 | -------------------------------------------------------------------------------- /old/src/infra/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/infra/repositories/__init__.py -------------------------------------------------------------------------------- /old/src/infra/repositories/base_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Iterable, Type 3 | 4 | from pydantic import BaseModel 5 | from sqlalchemy import ( 6 | Delete, 7 | Result, 8 | ScalarResult, 9 | Select, 10 | ValuesBase, 11 | delete, 12 | insert, 13 | select, 14 | update, 15 | ) 16 | from sqlalchemy.ext.asyncio import AsyncSession 17 | 18 | from old.src.infra.models.base_model import Base 19 | from src.lib import NotFoundError 20 | 21 | 22 | class Repository(ABC): 23 | """Абстрактный CRUD - репозиторий""" 24 | 25 | @abstractmethod 26 | async def create(self): 27 | pass 28 | 29 | @abstractmethod 30 | async def get_list(self, limit: int, offset: int): 31 | pass 32 | 33 | @abstractmethod 34 | async def get_one(self): 35 | pass 36 | 37 | @abstractmethod 38 | async def update(self): 39 | pass 40 | 41 | @abstractmethod 42 | async def delete(self): 43 | pass 44 | 45 | 46 | class SQLAlchemyRepository(Repository): 47 | """ 48 | CRUD - репозиторий для SQLAlchemy 49 | При инициализации наследников определяется модель и стандартное dto ответа 50 | """ 51 | 52 | model: Type[Base] = None 53 | response_dto: BaseModel = None 54 | 55 | def __init__(self, session: AsyncSession, auto_commit: bool = None, auto_refresh: bool = None): 56 | self.session = session 57 | self.auto_commit = auto_commit 58 | self.auto_refresh = auto_refresh 59 | 60 | async def create( 61 | self, 62 | create_dto: BaseModel, 63 | response_dto: Base | None = None, 64 | auto_commit: bool = True, 65 | ) -> BaseModel: 66 | stmt = insert(self.model).values(**create_dto.model_dump()).returning(self.model) 67 | res = await self._execute(stmt) 68 | await self._flush_or_commit(auto_commit) 69 | return self.to_dto(res.scalar_one(), response_dto) 70 | 71 | async def get_one(self, response_dto: Base | None = None, **filters) -> BaseModel: 72 | stmt = select(self.model).filter_by(**filters) 73 | result = await self._execute(stmt) 74 | instance = result.scalar_one_or_none() 75 | self.check_not_found(instance) 76 | return self.to_dto(instance, response_dto) 77 | 78 | async def get_list( 79 | self, 80 | response_dto: Base | None = None, 81 | limit: int = 100, 82 | offset: int = 0, 83 | order: str = "id", 84 | ) -> list[BaseModel]: 85 | stmt = select(self.model).order_by(order).limit(limit).offset(offset) 86 | res = await self._execute(stmt) 87 | return self.to_dto(res.scalars(), response_dto) 88 | 89 | async def update( 90 | self, 91 | update_dto: BaseModel, 92 | response_dto: Base | None = None, 93 | auto_commit: bool = True, 94 | **filters, 95 | ) -> BaseModel: 96 | stmt = update(self.model).values(**update_dto.model_dump()).filter_by(**filters).returning(self.model) 97 | res = (await self._execute(stmt)).scalar_one_or_none() 98 | self.check_not_found(res) 99 | await self._flush_or_commit(auto_commit) 100 | return self.to_dto(res, response_dto) 101 | 102 | async def delete(self, auto_commit: bool = True, **filters) -> None: 103 | stmt = delete(self.model).filter_by(**filters) 104 | result = await self._execute(stmt) 105 | if result.rowcount == 0: 106 | raise NotFoundError(f"По данным запроса в таблице {self.model.__tablename__} записей не найдено") 107 | await self._flush_or_commit(auto_commit) 108 | 109 | def to_dto(self, instance: Base | ScalarResult, dto: BaseModel = None) -> BaseModel | list[BaseModel]: 110 | """ 111 | Метод, преобразующий модели SQLAlchemy к dto. 112 | """ 113 | if dto is None: 114 | dto = self.response_dto 115 | if not isinstance(instance, ScalarResult | list): 116 | return dto.model_validate(instance, from_attributes=True) 117 | return [dto.model_validate(row, from_attributes=True) for row in instance] 118 | 119 | async def _flush_or_commit(self, auto_commit: bool | None) -> None: 120 | if auto_commit is None: 121 | auto_commit = self.auto_commit 122 | return await self.session.commit() if auto_commit else await self.session.flush() 123 | 124 | async def _refresh( 125 | self, 126 | instance: Base, 127 | auto_refresh: bool | None, 128 | attribute_names: Iterable[str] | None = None, 129 | with_for_update: bool | None = None, 130 | ) -> None: 131 | if auto_refresh is None: 132 | auto_refresh = self.auto_refresh 133 | 134 | return ( 135 | await self.session.refresh( 136 | instance, 137 | attribute_names=attribute_names, 138 | with_for_update=with_for_update, 139 | ) 140 | if auto_refresh 141 | else None 142 | ) 143 | 144 | @staticmethod 145 | def check_not_found(item_or_none: Base | None) -> Base: 146 | if item_or_none is None: 147 | msg = "No item found when one was expected" 148 | raise NotFoundError(msg) 149 | return item_or_none 150 | 151 | async def _execute(self, statement: ValuesBase | Select[Any] | Delete) -> Result[Any]: 152 | return await self.session.execute(statement) 153 | -------------------------------------------------------------------------------- /old/src/infra/repositories/email_repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select 2 | 3 | from old.src.domain.email.email_dto import CreateEmailCodeDTO 4 | 5 | from ..database.session import ISession 6 | from ..models.verify_email_model import VerifyEmailModel 7 | 8 | 9 | class EmailRepository: 10 | 11 | def __init__(self, session: ISession): 12 | self.session = session 13 | 14 | async def create(self, dto: CreateEmailCodeDTO): 15 | instance = VerifyEmailModel(**dto.model_dump()) 16 | self.session.add(instance) 17 | await self.session.commit() 18 | await self.session.refresh(instance) 19 | return instance 20 | 21 | async def get_code(self, code: str): 22 | stmt = select(VerifyEmailModel).filter_by(code=code) 23 | raw = await self.session.execute(stmt) 24 | return raw.scalar_one_or_none() 25 | -------------------------------------------------------------------------------- /old/src/infra/repositories/generic_alchemy_repo.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Type, TypeVar 2 | 3 | from pydantic import BaseModel 4 | from sqlalchemy import delete, select, update 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from ..models.base_model import Base 8 | 9 | 10 | ModelType = TypeVar("ModelType", bound=Base) 11 | CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) 12 | UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) 13 | 14 | 15 | class GenericSqlAlchemyRepository: 16 | """Репозиторий на основе Generic для работы с базой данных""" 17 | 18 | def __init__(self, model: Type[ModelType], db_session: AsyncSession): 19 | self._session_factory = db_session 20 | self.model = model 21 | 22 | async def create(self, data: CreateSchemaType) -> ModelType: 23 | async with self._session_factory() as session: 24 | instance = self.model(**data) 25 | session.add(instance) 26 | await session.commit() 27 | await session.refresh(instance) 28 | return instance 29 | 30 | async def update(self, data: UpdateSchemaType, **filters) -> ModelType: 31 | async with self._session_factory() as session: 32 | stmt = update(self.model).values(**data).filter_by(**filters).returning(self.model) 33 | res = await session.execute(stmt) 34 | await session.commit() 35 | return res.scalar_one() 36 | 37 | async def delete(self, **filters) -> None: 38 | async with self._session_factory() as session: 39 | await session.execute(delete(self.model).filter_by(**filters)) 40 | await session.commit() 41 | 42 | async def get_single(self, **filters) -> Optional[ModelType] | None: 43 | async with self._session_factory() as session: 44 | row = await session.execute(select(self.model).filter_by(**filters)) 45 | return row.scalar_one_or_none() 46 | 47 | async def get_multi(self, order: str = "id", limit: int = 100, offset: int = 0) -> list[ModelType]: 48 | async with self._session_factory() as session: 49 | stmt = select(self.model).order_by(order).limit(limit).offset(offset) 50 | row = await session.execute(stmt) 51 | return row.scalars().all() 52 | -------------------------------------------------------------------------------- /old/src/infra/repositories/invitation_repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select, update 2 | 3 | from old.src.domain.invitation.invitation_dto import InvitationCreateDTO 4 | 5 | from ..database.session import ISession 6 | from ..models.invite_registration import InviteRegistrationModel 7 | 8 | 9 | class InvitationRepository: 10 | 11 | def __init__(self, session: ISession): 12 | self.session = session 13 | 14 | async def create(self, dto: InvitationCreateDTO): 15 | instance = InviteRegistrationModel(**dto.model_dump()) 16 | self.session.add(instance) 17 | await self.session.commit() 18 | await self.session.refresh(instance) 19 | return instance 20 | 21 | async def get_list(self): 22 | stmt = select(InvitationModel) 23 | raw = await self.session.execute(stmt) 24 | return raw.scalars() 25 | 26 | async def update(self, status: str, pk: int): 27 | stmt = update(InvitationModel).values(status=status).filter_by(id=pk).returning(InvitationModel) 28 | raw = await self.session.execute(stmt) 29 | await self.session.commit() 30 | return raw.scalar_one() 31 | -------------------------------------------------------------------------------- /old/src/infra/repositories/login_repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select 2 | 3 | from ..database.session import ISession 4 | from ..models.user_model import UserModel 5 | 6 | 7 | class LoginRepository: 8 | 9 | def __init__(self, session: ISession): 10 | self.session = session 11 | 12 | async def get(self, email: str): 13 | stmt = select(UserModel).filter_by(email=email) 14 | raw = await self.session.execute(stmt) 15 | return raw.scalar_one_or_none() 16 | -------------------------------------------------------------------------------- /old/src/infra/repositories/permission_repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import delete, select, update 2 | 3 | from old.src.domain.permission.permission_dto import CreatePermissionDTO, UpdatePermissionDTO 4 | 5 | from ..database.session import ISession 6 | from ..models.permission_model import PermissionModel 7 | 8 | 9 | class PermissionRepository: 10 | def __init__(self, session: ISession): 11 | self.session = session 12 | 13 | async def create(self, dto: CreatePermissionDTO): 14 | instance = PermissionModel(**dto.model_dump()) 15 | self.session.add(instance) 16 | await self.session.commit() 17 | await self.session.refresh(instance) 18 | return instance 19 | 20 | async def get_list(self, limit: int): 21 | stmt = select(PermissionModel).limit(limit) 22 | raw = await self.session.execute(stmt) 23 | return raw.scalars() 24 | 25 | async def update(self, dto: UpdatePermissionDTO, pk: int): 26 | stmt = update(PermissionModel).values(**dto.model_dump()).filter_by(id=pk).returning(PermissionModel) 27 | raw = await self.session.execute(stmt) 28 | await self.session.commit() 29 | return raw.scalar_one() 30 | 31 | async def delete(self, pk: int) -> None: 32 | stmt = delete(PermissionModel).where(PermissionModel.id == pk) 33 | await self.session.execute(stmt) 34 | await self.session.commit() 35 | -------------------------------------------------------------------------------- /old/src/infra/repositories/role_repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import delete, select, update 2 | 3 | from old.src.domain.role.role_dto import CreateRoleDTO, UpdateRoleDTO 4 | 5 | from ..database.session import ISession 6 | from ..models.role_model import RoleModel 7 | 8 | 9 | class RoleRepository: 10 | def __init__(self, session: ISession): 11 | self.session = session 12 | 13 | async def create(self, dto: CreateRoleDTO): 14 | instance = RoleModel(**dto.model_dump()) 15 | self.session.add(instance) 16 | await self.session.commit() 17 | await self.session.refresh(instance) 18 | return instance 19 | 20 | async def get_list(self, limit: int): 21 | stmt = select(RoleModel).limit(limit) 22 | raw = await self.session.execute(stmt) 23 | return raw.scalars() 24 | 25 | async def get(self, pk: int): 26 | stmt = select(RoleModel).filter_by(id=pk) 27 | raw = await self.session.execute(stmt) 28 | return raw.scalar_one_or_none() 29 | 30 | async def update(self, dto: UpdateRoleDTO, pk: int): 31 | stmt = update(RoleModel).values(**dto.model_dump()).filter_by(id=pk).returning(RoleModel) 32 | raw = await self.session.execute(stmt) 33 | await self.session.commit() 34 | return raw.scalar_one() 35 | 36 | async def delete(self, pk: int) -> None: 37 | stmt = delete(RoleModel).where(RoleModel.id == pk) 38 | await self.session.execute(stmt) 39 | await self.session.commit() 40 | -------------------------------------------------------------------------------- /old/src/infra/repositories/user_repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select, update, delete 2 | 3 | from old.src.domain.user.user_entity import UserEntity 4 | from old.src.domain.user.user_dto import UpdateUserDTO, UserBaseDTO 5 | from sqlalchemy.exc import IntegrityError 6 | from src.lib import AlreadyExistError 7 | 8 | 9 | from ..database.session import ISession 10 | from ..models.user_model import UserModel 11 | 12 | 13 | class UserRepository: 14 | 15 | def __init__(self, session: ISession): 16 | self.session = session 17 | 18 | async def create(self, user: UserEntity): 19 | instance = UserModel(**user.__dict__) 20 | self.session.add(instance) 21 | try: 22 | await self.session.commit() 23 | except IntegrityError: 24 | raise AlreadyExistError(f'{instance.email} is already exist') 25 | await self.session.refresh(instance) 26 | return self._get_dto(instance) 27 | 28 | async def get_list(self, limit: int): 29 | stmt = select(UserModel).limit(limit) 30 | raw = await self.session.execute(stmt) 31 | return raw.scalars() 32 | 33 | async def get(self, pk: int): 34 | stmt = select(UserModel).filter_by(id=pk) 35 | raw = await self.session.execute(stmt) 36 | return raw.scalar_one_or_none() 37 | 38 | async def update(self, dto: UpdateUserDTO, pk: int): 39 | stmt = update(UserModel).values(**dto.model_dump()).filter_by(id=pk).returning(UserModel) 40 | raw = await self.session.execute(stmt) 41 | await self.session.commit() 42 | return raw.scalar_one() 43 | 44 | async def delete(self, pk: int) -> None: 45 | stmt = delete(UserModel).where(UserModel.id == pk) 46 | await self.session.execute(stmt) 47 | await self.session.commit() 48 | 49 | async def update_active(self, active: bool, pk: int): 50 | stmt = update(UserModel).values(active=active).filter_by(id=pk).returning(UserModel) 51 | raw = await self.session.execute(stmt) 52 | await self.session.commit() 53 | return raw.scalar_one() 54 | 55 | async def update_pass(self, new_password: str, pk: int): 56 | stmt = update(UserModel).values(password=new_password).filter_by(id=pk).returning(UserModel) 57 | raw = await self.session.execute(stmt) 58 | await self.session.commit() 59 | return raw.scalar_one() 60 | 61 | def _get_dto(self, row: UserModel) -> UserBaseDTO: 62 | return UserBaseDTO( 63 | surname=row.surname, 64 | name=row.name, 65 | email=row.email, 66 | name_telegram=row.name_telegram, 67 | nick_telegram=row.nick_telegram, 68 | nick_google_meet=row.nick_google_meet, 69 | nick_github=row.nick_github, 70 | nick_gitlab=row.nick_gitlab 71 | ) 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /old/src/infra/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/old/src/infra/services/__init__.py -------------------------------------------------------------------------------- /old/src/infra/services/email_service.py: -------------------------------------------------------------------------------- 1 | import aiosmtplib 2 | from email.mime.text import MIMEText 3 | from old.src.app.config.email_config import settings 4 | 5 | 6 | class EmailService: 7 | """Basic Service to send emails""" 8 | 9 | def __init__(self): 10 | # Credentials 11 | 12 | self.user = settings.email_username 13 | self.password = settings.email_password 14 | 15 | # Server config 16 | self.smtp_port = settings.smtp_port 17 | self.smtp_host = settings.smtp_host 18 | 19 | async def send_email(self, recipient_email: str, subject: str, body: str) -> None: 20 | # Message config 21 | message = MIMEText(body) 22 | message["Subject"] = subject 23 | message["From"] = self.user 24 | message['To'] = recipient_email 25 | 26 | # Email sending 27 | await aiosmtplib.send( 28 | message.as_string(), 29 | sender=self.user, 30 | recipients=recipient_email, 31 | hostname=self.smtp_host, 32 | port=self.smtp_port, 33 | username=self.user, 34 | password=self.password, 35 | 36 | ) 37 | 38 | 39 | -------------------------------------------------------------------------------- /old/src/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from old.src.app.config.project_config import settings as main_settings 4 | 5 | from old.src.app import get_application 6 | 7 | app = get_application() 8 | 9 | 10 | if __name__ == "__main__": 11 | uvicorn.run("main:app", host=main_settings.host, port=main_settings.port, reload=main_settings.debug) 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "SUP" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Omelchenko Michael "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | fastapi = "^0.105.0" 11 | uvicorn = {extras = ["standart"], version = "^0.23.2"} 12 | sqlalchemy = {extras = ["asyncio"], version = "^2.0.25"} 13 | alembic = "^1.13.1" 14 | pydantic-settings = "^2.1.0" 15 | argon2-cffi = "^23.1.0" 16 | pydantic = {extras = ["email"], version = "^2.5.3"} 17 | asyncpg = "^0.29.0" 18 | pyjwt = "^2.8.0" 19 | json-log-formatter = "^0.5.2" 20 | python-json-logger = "^2.0.7" 21 | aiosmtplib = "^3.0.1" 22 | pre-commit = "^3.7.0" 23 | typer = "^0.12.3" 24 | 25 | 26 | [tool.poetry.group.dev.dependencies] 27 | pytest = "^7.4.4" 28 | ruff = "^0.1.14" 29 | mypy = "^1.8.0" 30 | 31 | [tool.ruff] 32 | line-length = 120 33 | indent-width = 4 34 | exclude = ["./migrations", "./tests", "__init__.py"] 35 | 36 | [tool.ruff.lint.pycodestyle] 37 | max-line-length = 120 38 | 39 | [tool.ruff.lint] 40 | extend-ignore = ["F821"] 41 | select = ["F", "E", "I"] 42 | 43 | [tool.ruff.lint.isort] 44 | lines-after-imports = 2 45 | 46 | [tool.ruff.isort] 47 | known-first-party = ["src"] 48 | 49 | [tool.ruff.format] 50 | quote-style = "double" 51 | indent-style = "space" 52 | docstring-code-format = true 53 | docstring-code-line-length = 100 54 | 55 | [build-system] 56 | requires = ["poetry-core"] 57 | build-backend = "poetry.core.masonry.api" 58 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/__init__.py -------------------------------------------------------------------------------- /src/api/v1/auth/controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException, Request, Response 2 | 3 | from src.lib.exceptions import RegistrationError 4 | from src.api.v1.auth.dtos.registration import RegistrationDTO 5 | from src.apps.user.dto import UserBaseDTO 6 | from src.api.v1.auth.dtos.login import LoginDTO 7 | from src.apps.auth.dependends.service import IAuthService 8 | 9 | 10 | router = APIRouter(prefix="/auth", tags=["auth"]) 11 | 12 | 13 | @router.post("/registration", response_model=UserBaseDTO) 14 | async def registration(dto: RegistrationDTO, service: IAuthService, request: Request): 15 | """ 16 | controller for registration user 17 | """ 18 | try: 19 | return await service.registration(dto) 20 | except (RegistrationError, ValueError) as e: 21 | raise HTTPException(status_code=400, detail=str(e)) 22 | 23 | 24 | @router.post("/login") 25 | async def login(response: Response, dto: LoginDTO, service: IAuthService): 26 | try: 27 | tokens = await service.login(dto) 28 | response.set_cookie( 29 | key="access_token", 30 | value=tokens.access_token, 31 | httponly=True, 32 | samesite="none", 33 | secure=True, 34 | ) 35 | response.set_cookie( 36 | key="refresh_token", 37 | value=tokens.refresh_token, 38 | httponly=True, 39 | samesite="none", 40 | secure=True, 41 | ) 42 | return tokens 43 | except ValueError as e: 44 | raise HTTPException(status_code=400, detail=str(e)) 45 | -------------------------------------------------------------------------------- /src/api/v1/auth/dtos/login.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | 3 | 4 | class LoginDTO(BaseModel): 5 | email: EmailStr 6 | password: str 7 | -------------------------------------------------------------------------------- /src/api/v1/auth/dtos/registration.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pydantic import BaseModel, EmailStr, constr, model_validator 4 | 5 | 6 | class RegistrationDTO(BaseModel): 7 | name: constr(max_length=20) 8 | surname: constr(max_length=20) 9 | email: EmailStr 10 | password: constr(min_length=8) 11 | password2: constr(min_length=8) 12 | name_telegram: constr(max_length=50) 13 | nick_telegram: constr(max_length=50) 14 | nick_google_meet: constr(max_length=50) 15 | nick_gitlab: constr(max_length=50) 16 | nick_github: constr(max_length=50) 17 | 18 | @model_validator(mode="after") 19 | def code_validate(self): 20 | if self.password != self.password2: 21 | raise ValueError("password missmatch") 22 | reg = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!#%*?&]{6,20}$" 23 | pat = re.compile(reg) 24 | mat = re.search(pat, self.password) 25 | if not mat: 26 | raise ValueError( 27 | "password must contain minimum 8 characters, at least one capital letter, number and " 28 | "special character" 29 | ) 30 | return self 31 | -------------------------------------------------------------------------------- /src/api/v1/auth/dtos/token.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, constr 2 | 3 | 4 | class TokenDTO(BaseModel): 5 | access_token: str 6 | refresh_token: str 7 | -------------------------------------------------------------------------------- /src/api/v1/permission/routes.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/api/v1/permission/routes.py -------------------------------------------------------------------------------- /src/api/v1/project/routes.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/api/v1/project/routes.py -------------------------------------------------------------------------------- /src/api/v1/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from src.api.v1.auth.controller import router as auth 3 | from src.api.v1.user.controller import router as user 4 | 5 | 6 | router = APIRouter(prefix="/v1", tags=["v1"]) 7 | router.include_router(auth) 8 | router.include_router(user) 9 | -------------------------------------------------------------------------------- /src/api/v1/user/controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException, Request 2 | 3 | from src.apps.user.depenends.service import IUserService 4 | from src.apps.auth.exceptions.token import InvalidSignatureError 5 | 6 | router = APIRouter(prefix="/user", tags=["user"]) 7 | 8 | 9 | @router.get("/confirm/{token}") 10 | async def confirmation(token: str, service: IUserService, request: Request): 11 | """ 12 | controller for confirmation user 13 | """ 14 | try: 15 | await service.confirmation_user(token) 16 | except InvalidSignatureError as e: 17 | raise HTTPException(detail=str(e)) 18 | 19 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from contextlib import asynccontextmanager 4 | from pathlib import Path 5 | 6 | from fastapi import FastAPI 7 | 8 | from src.config.project import settings as main_settings 9 | from src.config.swagger import settings as swagger_settings 10 | from src.config.logging import settings as logger_settings, logger_config 11 | # 12 | from src.middleware import init_middleware 13 | from src.routes import get_apps_router 14 | # from old.src.app.routers import init_routers 15 | 16 | 17 | def get_description(path: Path | str) -> str: 18 | return path.read_text("UTF-8") if isinstance(path, Path) else Path(path).read_text("UTF-8") 19 | 20 | 21 | def get_application() -> FastAPI: 22 | if logger_settings.logging_on: 23 | logging.config.dictConfig(logger_config) # noqa 24 | application = FastAPI( 25 | title=swagger_settings.title, 26 | # description=get_description(swagger_settings.description), 27 | summary=swagger_settings.summary, 28 | version=main_settings.version, 29 | terms_of_service=swagger_settings.terms_of_service, 30 | contact=swagger_settings.contact, 31 | license_info=swagger_settings.license, 32 | # lifespan=lifespan, 33 | root_path=main_settings.root_path, 34 | debug=main_settings.debug, 35 | docs_url=swagger_settings.docs_url if main_settings.debug else None, 36 | redoc_url=swagger_settings.redoc_url if main_settings.debug else None, 37 | openapi_url=f"{swagger_settings.docs_url}/openapi.json" if main_settings.debug else None, 38 | ) 39 | init_middleware(application) 40 | application.include_router(get_apps_router()) 41 | return application -------------------------------------------------------------------------------- /src/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/apps/__init__.py -------------------------------------------------------------------------------- /src/apps/auth/dependends/service.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends 3 | 4 | from src.apps.auth.service import AuthService 5 | 6 | 7 | IAuthService = Annotated[AuthService, Depends()] 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/apps/auth/dependends/token_service.py: -------------------------------------------------------------------------------- 1 | from src.apps.auth.token_service import TokenService 2 | from typing import Annotated 3 | from fastapi import Depends 4 | 5 | 6 | ITokenService = Annotated[TokenService, Depends()] 7 | -------------------------------------------------------------------------------- /src/apps/auth/exceptions/token.py: -------------------------------------------------------------------------------- 1 | class InvalidSignatureError(Exception): 2 | pass 3 | 4 | 5 | class DecodeError(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /src/apps/auth/service.py: -------------------------------------------------------------------------------- 1 | from src.lib.exceptions import RegistrationError, AlreadyExistError 2 | from src.api.v1.auth.dtos.registration import RegistrationDTO 3 | from src.api.v1.auth.dtos.login import LoginDTO 4 | from src.apps.user.dto import FindUserDTO 5 | from src.apps.user.entity import UserEntity 6 | from src.apps.user.depenends.service import IUserService 7 | from src.apps.auth.dependends.token_service import ITokenService 8 | 9 | 10 | class AuthService: 11 | 12 | def __init__(self, user_service: IUserService, token_service: ITokenService): 13 | self.user_service = user_service 14 | self.token_service = token_service 15 | 16 | async def registration(self, dto: RegistrationDTO): 17 | registration_data = dto.model_dump() 18 | registration_data.pop('password2') 19 | user_entity = UserEntity(**registration_data) 20 | try: 21 | return await self.user_service.create(user_entity, email_confirmation=True) 22 | except AlreadyExistError as e: 23 | raise RegistrationError(e) 24 | 25 | async def login(self, dto: LoginDTO): 26 | user = await self.user_service.get_user(dto=FindUserDTO(email=dto.email)) 27 | dto_hash = UserEntity.hash_password(dto.password) 28 | if not user or user.password != dto_hash or not user.is_active: 29 | raise ValueError('Неверный логин или пароль') 30 | return await self.token_service.create_tokens(user) 31 | -------------------------------------------------------------------------------- /src/apps/auth/token_service.py: -------------------------------------------------------------------------------- 1 | from jwt import ExpiredSignatureError, PyJWTError, decode, encode, get_unverified_header 2 | from src.api.v1.auth.dtos.token import TokenDTO 3 | from datetime import datetime, timedelta 4 | from src.config.jwt_config import config_token 5 | from src.config.security import settings 6 | from src.apps.auth.exceptions.token import InvalidSignatureError 7 | 8 | 9 | class TokenService: 10 | 11 | def __init__(self) -> None: 12 | self.access_token_lifetime = config_token.ACCESS_TOKEN_LIFETIME 13 | self.refresh_token_lifetime = config_token.REFRESH_TOKEN_LIFETIME 14 | self.secret_key = settings.secret_key 15 | self.algorithm = settings.algorithm 16 | 17 | async def create_tokens(self, dto): 18 | access_token = await self.generate_access_token(dto) 19 | refresh_token = await self.generate_refresh_token(dto) 20 | return TokenDTO(access_token=access_token, refresh_token=refresh_token) 21 | 22 | def _validate_token(self, token: str): 23 | token_info = get_unverified_header(token) 24 | if token_info["alg"] != self.algorithm: 25 | raise InvalidSignatureError("Key error") 26 | return token 27 | 28 | async def encode_token(self, payload: dict) -> str: 29 | return encode(payload, self.secret_key, self.algorithm) 30 | 31 | async def decode_token(self, token: str) -> dict: 32 | try: 33 | self._validate_token(token) 34 | return decode(token, self.secret_key, self.algorithm) 35 | except ExpiredSignatureError: 36 | raise ExpiredSignatureError("Token lifetime is expired") 37 | except PyJWTError: 38 | raise Exception("Token is invalid") 39 | 40 | async def generate_access_token(self, dto): 41 | expire = datetime.now() + timedelta(seconds=self.access_token_lifetime) 42 | payload = { 43 | "token_type": "access", 44 | "user": {"user_id": str(dto.id), "user_name": str(dto.name)}, 45 | "exp": str(expire), 46 | "iat": str(datetime.now()), 47 | } 48 | return await self.encode_token(payload) 49 | 50 | async def generate_refresh_token(self, dto): 51 | expire = datetime.now() + timedelta(seconds=self.refresh_token_lifetime) 52 | payload = { 53 | "token_type": "access", 54 | "user": {"user_id": str(dto.id), "user_name": str(dto.name)}, 55 | "exp": str(expire), 56 | "iat": str(datetime.now()), 57 | } 58 | return await self.encode_token(payload) 59 | -------------------------------------------------------------------------------- /src/apps/email/dependends.py: -------------------------------------------------------------------------------- 1 | from src.apps.email.service import EmailService 2 | from typing import Annotated 3 | from fastapi import Depends 4 | 5 | 6 | IEmailService = Annotated[EmailService, Depends()] 7 | -------------------------------------------------------------------------------- /src/apps/email/service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiosmtplib 3 | from src.config.email import settings 4 | from email.message import EmailMessage 5 | 6 | 7 | class EmailService: 8 | 9 | async def send_message(self, to_email, subject, ms): 10 | message = EmailMessage() 11 | message["From"] = settings.smtp_server 12 | message["To"] = to_email 13 | message["Subject"] = subject 14 | message.set_content(ms) 15 | await aiosmtplib.send( 16 | message, 17 | hostname=settings.smtp_server, 18 | port=settings.smtp_port, 19 | username=settings.smtp_user, 20 | password=settings.smtp_password 21 | ) 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/apps/invitation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/apps/invitation/__init__.py -------------------------------------------------------------------------------- /src/apps/invitation/dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr 2 | from datetime import datetime 3 | 4 | 5 | class InvintationDTO(BaseModel): 6 | title: constr(max_length=20) 7 | code: constr(max_length=20) 8 | is_active: bool 9 | finish_at: datetime 10 | author_id: int = None 11 | 12 | -------------------------------------------------------------------------------- /src/apps/invitation/exceptions.py: -------------------------------------------------------------------------------- 1 | class InviteError(Exception): 2 | pass -------------------------------------------------------------------------------- /src/apps/invitation/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from sqlalchemy import ForeignKey, String 3 | from sqlalchemy.orm import Mapped, mapped_column 4 | from src.lib.base_model import Base 5 | 6 | 7 | class InviteRegistrationModel(Base): 8 | """Модель приглашения 9 | 10 | :param id: идентификатор 11 | :param title: название митапа 12 | :param author_id: автор пользователя 13 | :param code: код ссылки 14 | :param finish_at: дата окончания ссылки 15 | """ 16 | __tablename__ = "invite_registation" 17 | 18 | title: Mapped[str] 19 | finish_at: Mapped[datetime] 20 | code: Mapped[str] = mapped_column(String(50), unique=True, index=True) 21 | is_active: Mapped[bool] 22 | author_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) 23 | -------------------------------------------------------------------------------- /src/apps/invitation/repository.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select, update, delete 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from src.lib.exceptions import AlreadyExistError 5 | from src.config.database.session import ISession 6 | from src.apps.invitation.models import InviteRegistrationModel 7 | 8 | 9 | class InvintationRepository: 10 | 11 | def __init__(self, session: ISession): 12 | self.session = session 13 | 14 | async def create(self, dto): 15 | instance = InviteRegistrationModel(**dto.model_dump()) 16 | self.session.add(instance) 17 | try: 18 | await self.session.commit() 19 | except IntegrityError: 20 | raise AlreadyExistError(f'{instance.code} is already exist') 21 | await self.session.refresh(instance) 22 | return self._get_dto(instance) 23 | 24 | def _get_dto(self, row): 25 | print(row) 26 | 27 | -------------------------------------------------------------------------------- /src/apps/invitation/service.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from src.apps.invitation.exceptions import InviteError 4 | from src.apps.email.dependends import IEmailService 5 | from old.src.domain.invitation.invitation_dto import InvitationCreateDTO 6 | from old.src.domain.invitation.invitation_entity import InvitationEntity 7 | 8 | 9 | class InvitationService: 10 | 11 | def __init__(self, repository, email_service: IEmailService): 12 | self.repository = repository 13 | self.email_service = email_service 14 | 15 | async def create(self, dto): 16 | print(dto) 17 | # invite = InvitationEntity() 18 | # code = invite.generate_code() 19 | # date = invite.generation_date() 20 | # dto = InvitationCreateDTO(code=code, at_valid=date) 21 | # return await self.repository.create(dto) 22 | 23 | async def get_list(self): 24 | return await self.repository.get_list() 25 | 26 | async def check(self, code: str): 27 | invite = await self.repository.get(code) 28 | at_date = date.today() 29 | if invite is None: 30 | raise InviteError("Not found") 31 | elif at_date > invite.at_valid: 32 | await self.repository.update("expired", invite.id) 33 | raise InviteError("Invalid date") 34 | elif invite.status == "visited": 35 | raise InviteError("Invalid status") 36 | 37 | invite = await self.repository.update("visited", invite.id) 38 | return invite 39 | -------------------------------------------------------------------------------- /src/apps/permissions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/apps/permissions/__init__.py -------------------------------------------------------------------------------- /src/apps/permissions/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .project import ProjectMemberPermission 2 | from .user import UserPermissionModel 3 | from .permission import PermissionModel 4 | -------------------------------------------------------------------------------- /src/apps/permissions/models/permission.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from src.lib.base_model import Base 7 | 8 | 9 | class PermissionModel(Base): 10 | """Модель прав доступа 11 | 12 | :param id: идентификатор 13 | :param title: название права доступа 14 | :param code: код права доступа 15 | :param description: описание прав доступа 16 | :param users: все юзеры, у которых есть это право доступа 17 | :param project_members: все участники проекта, у которых есть это право доступа 18 | 19 | """ 20 | __tablename__ = "permissions" 21 | 22 | title: Mapped[str] = mapped_column(String(20)) 23 | code: Mapped[int] = mapped_column(unique=True) 24 | description: Mapped[str] = mapped_column(nullable=True) 25 | users: Mapped[List["UserModel"]] = relationship( 26 | back_populates="permissions", 27 | secondary="user_permission", 28 | lazy="raise_on_sql" 29 | ) -------------------------------------------------------------------------------- /src/apps/permissions/models/project.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from src.lib.base_model import Base 5 | 6 | 7 | class ProjectMemberPermission(Base): 8 | """ Модель разрешений участника проекта 9 | 10 | :param id: идентификатор 11 | :param member_id: id участника проекта 12 | :param permission_id: id разрешения 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "member_permission" 17 | member_id: Mapped[int] = mapped_column(ForeignKey( 18 | "project_user.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | permission_id: Mapped[int] = mapped_column(ForeignKey( 24 | "permissions.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) -------------------------------------------------------------------------------- /src/apps/permissions/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from src.lib.base_model import Base 5 | 6 | 7 | class UserPermissionModel(Base): 8 | """ Модель разрешений пользователя 9 | 10 | :param id: идентификатор 11 | :param user_id: id пользователя 12 | :param permission_id: id разрешения 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "user_permission" 17 | user_id: Mapped[int] = mapped_column(ForeignKey( 18 | "users.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | permission_id: Mapped[int] = mapped_column(ForeignKey( 24 | "permissions.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | -------------------------------------------------------------------------------- /src/apps/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/apps/project/__init__.py -------------------------------------------------------------------------------- /src/apps/project/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .feature import FeatureModel 2 | from .project import ProjectModel 3 | from .task import TaskModel 4 | from .task_member import TaskMemberModel 5 | from .feature_member import FeatureMemberModel 6 | from .project_member import ProjectUserModel 7 | -------------------------------------------------------------------------------- /src/apps/project/models/feature.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey, Enum 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import Literal, get_args, List 4 | 5 | 6 | from src.lib.base_model import Base 7 | 8 | 9 | priority_status = Literal["low", "medium", "high"] 10 | activity_status = Literal["discussion", "development", "closed"] 11 | 12 | 13 | class FeatureModel(Base): 14 | """ Модель фич проектов 15 | 16 | :param id: идентификатор 17 | :param user_id: id пользователя 18 | :param permission_id: id разрешения 19 | :param created_at: дата создания 20 | :param updated_at: дата обновления 21 | """ 22 | __tablename__ = "feature" 23 | project_id: Mapped[int] = mapped_column(ForeignKey( 24 | "project.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | priority: Mapped[priority_status] = mapped_column(Enum( 30 | *get_args(priority_status), 31 | name="priority", 32 | create_constraint=True, 33 | validate_strings=True, 34 | )) 35 | status: Mapped[activity_status] = mapped_column(Enum( 36 | *get_args(activity_status), 37 | name="status", 38 | create_constraint=True, 39 | validate_strings=True, 40 | )) 41 | title: Mapped[str] 42 | description: Mapped[str] 43 | members: Mapped[List["ProjectUserModel"]] = relationship( 44 | back_populates="members", 45 | secondary="task_member", 46 | lazy="raise_on_sql" 47 | ) 48 | tags: Mapped[List["FeatureTagModel"]] = relationship( 49 | back_populates="tags", 50 | lazy="raise_on_sql" 51 | ) 52 | -------------------------------------------------------------------------------- /src/apps/project/models/feature_member.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import List 4 | 5 | from src.lib.base_model import Base 6 | 7 | 8 | class FeatureMemberModel(Base): 9 | """ Модель участника фичи 10 | 11 | :param id: идентификатор 12 | :param member_id: id участника проекта 13 | :param feature_id: id фичи 14 | :param is_responsible: Ответственный ли этот пользователь за реализацию фичи 15 | :param created_at: дата создания 16 | :param updated_at: дата обновления 17 | """ 18 | __tablename__ = "feature_member" 19 | member_id: Mapped[int] = mapped_column(ForeignKey( 20 | "project_user.id", 21 | ondelete="CASCADE", 22 | ), 23 | primary_key=True 24 | ) 25 | feature_id: Mapped[int] = mapped_column(ForeignKey( 26 | "feature.id", 27 | ondelete="CASCADE", 28 | ), 29 | primary_key=True 30 | ) 31 | is_responsible: Mapped[bool] = mapped_column( 32 | default=False, 33 | nullable=False 34 | ) 35 | -------------------------------------------------------------------------------- /src/apps/project/models/project.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship 2 | from typing import List 3 | 4 | from src.lib.base_model import Base 5 | 6 | 7 | class ProjectModel(Base): 8 | """Модель проекта 9 | 10 | :param id: идентификатор 11 | :param title: название проекта 12 | :param description: описание проекта 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "project" 17 | 18 | title: Mapped[str] 19 | description: Mapped[str] 20 | users: Mapped[List["UserModel"]] = relationship( 21 | back_populates="project", 22 | secondary="project_user", 23 | lazy="raise_on_sql" 24 | ) -------------------------------------------------------------------------------- /src/apps/project/models/project_member.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import List 4 | 5 | from src.lib.base_model import Base 6 | 7 | 8 | class ProjectUserModel(Base): 9 | """ Модель участника проекта 10 | 11 | :param id: идентификатор 12 | :param user_id: id пользователя 13 | :param project_id: id проекта 14 | :param is_responsible: Ответственный ли этот пользователь за проект 15 | :param permissions: список прав участника 16 | :param created_at: дата создания 17 | :param updated_at: дата обновления 18 | """ 19 | __tablename__ = "project_user" 20 | user_id: Mapped[int] = mapped_column(ForeignKey( 21 | "users.id", 22 | ondelete="CASCADE", 23 | ), 24 | primary_key=True 25 | ) 26 | project_id: Mapped[int] = mapped_column(ForeignKey( 27 | "project.id", 28 | ondelete="CASCADE", 29 | ), 30 | primary_key=True 31 | ) 32 | is_responsible: Mapped[bool] = mapped_column( 33 | default=False, 34 | nullable=False 35 | ) 36 | permissions: Mapped[List["Permission"]] = relationship( 37 | back_populates="permissions", 38 | secondary="member_permission", 39 | lazy="raise_on_sql" 40 | ) -------------------------------------------------------------------------------- /src/apps/project/models/task.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped, relationship, mapped_column 2 | from sqlalchemy import ForeignKey, Enum 3 | from typing import List, Literal, get_args 4 | 5 | from src.lib.base_model import Base 6 | 7 | status = Literal["discussion", "development", "closed"] 8 | 9 | 10 | class TaskModel(Base): 11 | """Модель проекта 12 | 13 | :param id: идентификатор 14 | :param title: название проекта 15 | :param description: описание проекта 16 | :param created_at: дата создания 17 | :param updated_at: дата обновления 18 | """ 19 | __tablename__ = "task" 20 | title: Mapped[str] 21 | description: Mapped[str] 22 | status: Mapped[status] = mapped_column(Enum( 23 | *get_args(status), 24 | name="status", 25 | create_constraint=True, 26 | validate_strings=True, 27 | )) 28 | feature_id: Mapped[int] = mapped_column(ForeignKey( 29 | "feature.id", 30 | ondelete="CASCADE", 31 | ), 32 | primary_key=True 33 | ) 34 | members: Mapped[List["Permission"]] = relationship( 35 | back_populates="permissions", 36 | secondary="member_permission", 37 | lazy="raise_on_sql" 38 | ) 39 | tags: Mapped[List["TaskTagModel"]] = relationship( 40 | back_populates="tags", 41 | lazy="raise_on_sql" 42 | ) 43 | -------------------------------------------------------------------------------- /src/apps/project/models/task_member.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from src.lib.base_model import Base 5 | 6 | 7 | class TaskMemberModel(Base): 8 | """ Модель участника таски 9 | 10 | :param id: идентификатор 11 | :param member_id: id участника проекта 12 | :param task_id: id таски 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "task_member" 17 | member_id: Mapped[int] = mapped_column(ForeignKey( 18 | "project_user.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | is_responsible: Mapped[bool] = mapped_column( 24 | default=False, 25 | nullable=False 26 | ) 27 | task_id: Mapped[int] = mapped_column(ForeignKey( 28 | "task.id", 29 | ondelete="CASCADE", 30 | ), 31 | primary_key=True 32 | ) -------------------------------------------------------------------------------- /src/apps/tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/apps/tags/__init__.py -------------------------------------------------------------------------------- /src/apps/tags/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .feature import FeatureTagModel 2 | from .tag import TagModel 3 | from .task import TaskTagModel 4 | -------------------------------------------------------------------------------- /src/apps/tags/models/feature.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from src.lib.base_model import Base 5 | 6 | 7 | class FeatureTagModel(Base): 8 | """ Модель тего фичи 9 | 10 | :param id: идентификатор 11 | :param tag_id: id тега 12 | :param feature_id: id фичи 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "feature_tag" 17 | tag_id: Mapped[int] = mapped_column(ForeignKey( 18 | "tag.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | feature_id: Mapped[int] = mapped_column(ForeignKey( 24 | "feature.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | -------------------------------------------------------------------------------- /src/apps/tags/models/tag.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey, Enum 2 | from sqlalchemy.orm import mapped_column, Mapped, relationship 3 | from typing import Literal, get_args, List 4 | 5 | 6 | from src.lib.base_model import Base 7 | 8 | 9 | class TagModel(Base): 10 | """ Модель фич проектов 11 | 12 | :param id: идентификатор 13 | :param title: название тега 14 | :param created_at: дата создания 15 | :param updated_at: дата обновления 16 | """ 17 | __tablename__ = "tag" 18 | title: Mapped[str] 19 | -------------------------------------------------------------------------------- /src/apps/tags/models/task.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ForeignKey 2 | from sqlalchemy.orm import mapped_column, Mapped 3 | 4 | from src.lib.base_model import Base 5 | 6 | 7 | class TaskTagModel(Base): 8 | """ Модель тега для таски 9 | 10 | :param id: идентификатор 11 | :param tag_id: id тега 12 | :param task_id: id таски 13 | :param created_at: дата создания 14 | :param updated_at: дата обновления 15 | """ 16 | __tablename__ = "task_tag" 17 | tag_id: Mapped[int] = mapped_column(ForeignKey( 18 | "tag.id", 19 | ondelete="CASCADE", 20 | ), 21 | primary_key=True 22 | ) 23 | task_id: Mapped[int] = mapped_column(ForeignKey( 24 | "task.id", 25 | ondelete="CASCADE", 26 | ), 27 | primary_key=True 28 | ) 29 | -------------------------------------------------------------------------------- /src/apps/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/apps/user/__init__.py -------------------------------------------------------------------------------- /src/apps/user/depenends/repository.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends 3 | 4 | from src.apps.user.repositories.user import UserRepository 5 | 6 | 7 | IUserRepository = Annotated[UserRepository, Depends()] 8 | -------------------------------------------------------------------------------- /src/apps/user/depenends/service.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends 3 | 4 | from src.apps.user.service import UserService 5 | 6 | 7 | IUserService = Annotated[UserService, Depends()] 8 | -------------------------------------------------------------------------------- /src/apps/user/dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, constr 2 | 3 | 4 | class FindUserDTO(BaseModel): 5 | id: int = None 6 | name: constr(max_length=20) = None 7 | surname: constr(max_length=20) = None 8 | email: EmailStr = None 9 | name_telegram: constr(max_length=50) = None 10 | nick_telegram: constr(max_length=50) = None 11 | nick_google_meet: constr(max_length=50) = None 12 | nick_gitlab: constr(max_length=50) = None 13 | nick_github: constr(max_length=50) = None 14 | 15 | 16 | class UserBaseDTO(BaseModel): 17 | name: constr(max_length=20) 18 | surname: constr(max_length=20) 19 | email: EmailStr 20 | name_telegram: constr(max_length=50) 21 | nick_telegram: constr(max_length=50) 22 | nick_google_meet: constr(max_length=50) 23 | nick_gitlab: constr(max_length=50) 24 | nick_github: constr(max_length=50) 25 | role_id: int = None 26 | 27 | 28 | class UserDTO(UserBaseDTO): 29 | id: int 30 | is_active: bool 31 | password: str 32 | 33 | 34 | class CreateUserDTO(UserBaseDTO): 35 | pass 36 | 37 | 38 | class GetUserListDTO(UserBaseDTO): 39 | id: int 40 | 41 | 42 | class GetUserDTO(UserBaseDTO): 43 | id: int 44 | 45 | 46 | class UpdateUserDTO(UserBaseDTO): 47 | pass 48 | 49 | 50 | class UpdatePasswordDTO(BaseModel): 51 | password: str -------------------------------------------------------------------------------- /src/apps/user/entity.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | from dataclasses import dataclass 4 | from src.config.security import settings 5 | from argon2 import PasswordHasher 6 | 7 | from pydantic import EmailStr 8 | 9 | 10 | @dataclass 11 | class UserEntity: 12 | name: str 13 | surname: str 14 | email: EmailStr 15 | name_telegram: str 16 | nick_telegram: str 17 | nick_google_meet: str 18 | nick_gitlab: str 19 | nick_github: str 20 | role_id: int | None = None 21 | password: str | None = None 22 | is_admin: bool | None = None 23 | 24 | def get_new_hash_password(self): 25 | password = self.generate_password() 26 | self.password = self.hash_password(password) 27 | return self 28 | 29 | def __post_init__(self): 30 | password = self.hash_password(self.password) 31 | self.password = password 32 | 33 | @staticmethod 34 | def hash_password(password: str) -> str: 35 | salt = settings.secret_key 36 | hashed = PasswordHasher().hash(password.encode("utf-8"), salt=salt.encode("utf-8")) 37 | return hashed 38 | 39 | @classmethod 40 | def set_password(cls, password): 41 | return cls.hash_password(password) 42 | 43 | def create_verify_code(self, length=16): 44 | character_sheet = string.ascii_letters + string.digits 45 | return "".join(secrets.choice(character_sheet) for i in range(length)) 46 | -------------------------------------------------------------------------------- /src/apps/user/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import UserModel 2 | from .role import RoleModel 3 | -------------------------------------------------------------------------------- /src/apps/user/models/role.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Mapped 2 | 3 | from src.lib.base_model import Base 4 | 5 | 6 | class RoleModel(Base): 7 | """Модель роли 8 | 9 | :param id: идентификатор 10 | :param name: название роли 11 | :param color: цвет роли 12 | :param created_at: дата создания 13 | :param updated_at: дата обновления 14 | """ 15 | __tablename__ = "roles" 16 | 17 | name: Mapped[str] 18 | color: Mapped[str] 19 | -------------------------------------------------------------------------------- /src/apps/user/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import ForeignKey, String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from src.lib.base_model import Base 7 | from src.apps.permissions.models.user import UserPermissionModel 8 | 9 | 10 | class UserModel(Base): 11 | """Модель пользователя 12 | 13 | :param id: идентификатор 14 | :param name: имя пользователя 15 | :param surname: фамилия пользователя 16 | :param email: email пользователя 17 | :param password: пароль пользователя 18 | :param avatar_link: ссылка на аватар пользователя 19 | :param name_telegram: имя в телеграм 20 | :param nick_telegram: ник в телеграм 21 | :param nick_google_meet: ник в google meet 22 | :param nick_gitlab: ник в gitlab 23 | :param nick_github: ник в github 24 | :param is_active: активирован ли пользователь 25 | :param is_admin: является ли пользователь админом 26 | :param role_id: роль пользователя 27 | :param permissions: список прав пользователя 28 | :param created_at: дата создания 29 | :param updated_at: дата обновления 30 | """ 31 | __tablename__ = "users" 32 | 33 | name: Mapped[str] = mapped_column(String(20)) 34 | surname: Mapped[str] = mapped_column(String(20), nullable=True) 35 | email: Mapped[str] = mapped_column(String(50), unique=True, index=True) 36 | password: Mapped[str] 37 | avatar_link: Mapped[str] = mapped_column(String(50), nullable=True) 38 | name_telegram: Mapped[str] = mapped_column(String(50)) 39 | nick_telegram: Mapped[str] = mapped_column(String(50)) 40 | nick_google_meet: Mapped[str] = mapped_column(String(50)) 41 | nick_gitlab: Mapped[str] = mapped_column(String(50)) 42 | nick_github: Mapped[str] = mapped_column(String(50)) 43 | is_active: Mapped[bool] = mapped_column(default=False) 44 | is_admin: Mapped[bool] = mapped_column(default=False) 45 | role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=True) 46 | permissions: Mapped[List["PermissionModel"]] = relationship( 47 | back_populates="users", 48 | secondary="user_permission", 49 | # lazy="raise_on_sql" 50 | ) 51 | -------------------------------------------------------------------------------- /src/apps/user/repositories/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select, update, delete 2 | from sqlalchemy.exc import IntegrityError 3 | 4 | from src.lib.exceptions import AlreadyExistError 5 | from src.apps.user.entity import UserEntity 6 | from src.config.database.session import ISession 7 | from src.apps.user.models.user import UserModel 8 | from src.apps.user.dto import UpdateUserDTO, UserDTO, FindUserDTO 9 | 10 | 11 | class UserRepository: 12 | model = UserModel 13 | 14 | def __init__(self, session: ISession): 15 | self.session = session 16 | 17 | async def create(self, user: UserEntity): 18 | instance = UserModel(**user.__dict__) 19 | self.session.add(instance) 20 | try: 21 | await self.session.commit() 22 | except IntegrityError: 23 | raise AlreadyExistError(f'{instance.email} is already exist') 24 | await self.session.refresh(instance) 25 | return self._get_dto(instance) 26 | 27 | async def get_user(self, dto: FindUserDTO): 28 | stmt = select(self.model).filter_by(**dto.model_dump(exclude_none=True)) 29 | raw = await self.session.execute(stmt) 30 | result = raw.scalar_one_or_none() 31 | return self._get_dto(result) if result else None 32 | 33 | async def get_list(self, limit: int): 34 | stmt = select(UserModel).limit(limit) 35 | raw = await self.session.execute(stmt) 36 | return raw.scalars() 37 | 38 | async def get(self, pk: int): 39 | stmt = select(UserModel).filter_by(id=pk) 40 | raw = await self.session.execute(stmt) 41 | return raw.scalar_one_or_none() 42 | 43 | async def update(self, dto: UpdateUserDTO, pk: int): 44 | stmt = update(UserModel).values(**dto.model_dump()).filter_by(id=pk).returning(UserModel) 45 | raw = await self.session.execute(stmt) 46 | await self.session.commit() 47 | return raw.scalar_one() 48 | 49 | async def delete(self, pk: int) -> None: 50 | stmt = delete(UserModel).where(UserModel.id == pk) 51 | await self.session.execute(stmt) 52 | await self.session.commit() 53 | 54 | async def update_active(self, active: bool, pk: int): 55 | stmt = update(UserModel).values(is_active=active).filter_by(id=pk).returning(UserModel) 56 | raw = await self.session.execute(stmt) 57 | await self.session.commit() 58 | return raw.scalar_one() 59 | 60 | async def update_pass(self, new_password: str, pk: int): 61 | stmt = update(UserModel).values(password=new_password).filter_by(id=pk).returning(UserModel) 62 | raw = await self.session.execute(stmt) 63 | await self.session.commit() 64 | return raw.scalar_one() 65 | 66 | def _get_dto(self, row: UserModel) -> UserDTO: 67 | return UserDTO( 68 | id=row.id, 69 | is_active=row.is_active, 70 | surname=row.surname, 71 | password=row.password, 72 | name=row.name, 73 | email=row.email, 74 | name_telegram=row.name_telegram, 75 | nick_telegram=row.nick_telegram, 76 | nick_google_meet=row.nick_google_meet, 77 | nick_github=row.nick_github, 78 | nick_gitlab=row.nick_gitlab 79 | ) -------------------------------------------------------------------------------- /src/apps/user/service.py: -------------------------------------------------------------------------------- 1 | from src.apps.user.depenends.repository import IUserRepository 2 | from src.apps.user.dto import FindUserDTO, UserBaseDTO, UserDTO 3 | from src.apps.user.entity import UserEntity 4 | from src.apps.auth.dependends.token_service import ITokenService 5 | from src.apps.email.dependends import IEmailService 6 | from datetime import datetime, timedelta 7 | 8 | 9 | class UserService: 10 | 11 | def __init__(self, repository: IUserRepository, token_service: ITokenService, email_service: IEmailService): 12 | self.repository = repository 13 | self.token_service = token_service 14 | self.email_service = email_service 15 | 16 | async def _generate_confirm_link(self, user): 17 | #TODO продумать как не хардкодить домен 18 | expire = datetime.now() + timedelta(days=7) 19 | payload = { 20 | "id": user.id, 21 | "expire": str(expire) 22 | } 23 | domain = 'http://localhost:8000' 24 | token = await self.token_service.encode_token(payload=payload) 25 | link = f'{domain}/v1/user/confirm/{token}' 26 | return link 27 | 28 | async def get_user(self, dto: FindUserDTO) -> UserDTO: 29 | user = await self.repository.get_user(dto=dto) 30 | return user 31 | 32 | async def create(self, dto: UserEntity, email_confirmation=False) ->UserBaseDTO: 33 | user = await self.repository.create(dto) 34 | if email_confirmation: 35 | link = await self._generate_confirm_link(user) 36 | await self.email_service.send_message(user.email, subject='confirmation link', ms=link) 37 | return UserBaseDTO( 38 | surname=user.surname, 39 | name=user.name, 40 | email=user.email, 41 | name_telegram=user.name_telegram, 42 | nick_telegram=user.nick_telegram, 43 | nick_google_meet=user.nick_google_meet, 44 | nick_github=user.nick_github, 45 | nick_gitlab=user.nick_gitlab 46 | ) 47 | 48 | async def confirmation_user(self, token: str): 49 | token = await self.token_service.decode_token(token) 50 | expire_time = datetime.strptime(token['expire'], '%Y-%m-%d %H:%M:%S.%f') 51 | if datetime.now() < expire_time: 52 | await self.repository.update_active(pk=token['id'], active=True) 53 | return True 54 | return False -------------------------------------------------------------------------------- /src/config/cors.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List, Any, Type, Tuple 3 | 4 | from pydantic import Field 5 | from pydantic.fields import FieldInfo 6 | 7 | from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource 8 | 9 | 10 | class MyCustomSource(EnvSettingsSource): 11 | def prepare_field_value( 12 | self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool 13 | ) -> Any: 14 | if field_name in ["allow_origins", "allow_methods", "allow_headers", "expose_headers"]: 15 | if value: 16 | return value.split(",") 17 | return json.loads(value) if value else value 18 | 19 | 20 | class Settings(BaseSettings): 21 | allow_origins: List[str] = Field(default=["*"], alias="CORS_ALLOW_ORIGINS") 22 | allow_methods: List[str] = Field(default=["GET"], alias="CORS_ALLOW_METHODS") 23 | allow_headers: List[str] = Field(default=["*"], alias="CORS_ALLOW_HEADERS") 24 | allow_credentials: bool = Field(default=False, alias="CORS_ALLOW_CREDENTIALS") 25 | allow_origin_regex: str | None = Field(None, alias="CORS_ALLOW_ORIGIN_REGEX") 26 | expose_headers: List[str] = Field(default=["*"], alias="CORS_EXPOSE_HEADERS") 27 | max_age: int = Field(default=600, alias="CORS_MAX_AGE") 28 | 29 | @classmethod 30 | def settings_customise_sources( 31 | cls, 32 | settings_cls: Type[BaseSettings], 33 | init_settings: PydanticBaseSettingsSource, 34 | env_settings: PydanticBaseSettingsSource, 35 | dotenv_settings: PydanticBaseSettingsSource, 36 | file_secret_settings: PydanticBaseSettingsSource, 37 | ) -> Tuple[PydanticBaseSettingsSource, ...]: 38 | return (MyCustomSource(settings_cls),) 39 | 40 | 41 | settings = Settings() -------------------------------------------------------------------------------- /src/config/database/engine.py: -------------------------------------------------------------------------------- 1 | from asyncio import current_task 2 | from contextlib import asynccontextmanager 3 | 4 | from sqlalchemy.ext.asyncio import ( 5 | AsyncSession, 6 | create_async_engine, 7 | async_sessionmaker, 8 | async_scoped_session 9 | ) 10 | 11 | from src.config.database.settings import settings 12 | 13 | 14 | class DatabaseHelper: 15 | """Класс для работы с базой данных 16 | """ 17 | def __init__(self, url: str, echo: bool = False): 18 | self.engine = create_async_engine(url=url, echo=echo) 19 | 20 | self.session_factory = async_sessionmaker( 21 | bind=self.engine, 22 | autoflush=False, 23 | autocommit=False, 24 | expire_on_commit=False 25 | ) 26 | 27 | def get_scope_session(self): 28 | return async_scoped_session( 29 | session_factory=self.session_factory, 30 | scopefunc=current_task 31 | ) 32 | 33 | @asynccontextmanager 34 | async def get_db_session(self): 35 | from sqlalchemy import exc 36 | 37 | session: AsyncSession = self.session_factory() 38 | try: 39 | yield session 40 | except exc.SQLAlchemyError: 41 | await session.rollback() 42 | raise 43 | finally: 44 | await session.close() 45 | 46 | async def get_session(self): 47 | from sqlalchemy import exc 48 | 49 | session: AsyncSession = self.session_factory() 50 | try: 51 | yield session 52 | except exc.SQLAlchemyError: 53 | await session.rollback() 54 | raise 55 | finally: 56 | await session.close() 57 | 58 | 59 | db_helper = DatabaseHelper(settings.database_url, settings.db_echo_log) 60 | -------------------------------------------------------------------------------- /src/config/database/session.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Depends 3 | from sqlalchemy.ext.asyncio import AsyncSession 4 | 5 | from src.config.database.engine import db_helper 6 | 7 | ISession = Annotated[AsyncSession, Depends(db_helper.get_session)] 8 | -------------------------------------------------------------------------------- /src/config/database/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import PostgresDsn, Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | db_url_scheme: str = Field("postgresql+asyncpg", alias="DB_URL_SCHEME") 7 | # host 8 | db_host: str = Field(..., alias="DB_HOST") 9 | db_port: str = Field(..., alias="DB_PORT") 10 | db_name: str = Field(..., alias="DB_NAME") 11 | # credentials 12 | db_user: str = Field(..., alias="DB_USER") 13 | db_password: str = Field(..., alias="DB_PASSWORD") 14 | # logging 15 | db_echo_log: bool = Field(False, alias="DB_ECHO_LOG") 16 | # run auto-migrate 17 | db_run_auto_migrate: bool = Field(False, alias="DB_RUN_AUTO_MIGRATE") 18 | 19 | @property 20 | def database_url(self) -> PostgresDsn: 21 | """ URL для подключения (DSN)""" 22 | return ( 23 | f"{self.db_url_scheme}://{self.db_user}:{self.db_password}@" 24 | f"{self.db_host}:{self.db_port}/{self.db_name}?async_fallback=True" 25 | ) 26 | 27 | 28 | settings = Settings() 29 | -------------------------------------------------------------------------------- /src/config/email.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | # credentials 7 | smtp_user: str = Field(..., alias="SMTP_USER") 8 | smtp_password: str = Field(..., alias="SMTP_PASSWORD") 9 | smtp_port: int = Field(587, alias="SMTP_PORT") 10 | smtp_server: str = Field("smtp.gmail.com", alias="SMTP_HOST") 11 | 12 | 13 | settings = Settings() 14 | -------------------------------------------------------------------------------- /src/config/jwt_config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | 3 | 4 | class ConfigToken(BaseSettings): 5 | ACCESS_TOKEN_LIFETIME: int 6 | REFRESH_TOKEN_LIFETIME: int 7 | REFRESH_TOKEN_ROTATE_MIN_LIFETIME: int 8 | 9 | 10 | config_token = ConfigToken() 11 | -------------------------------------------------------------------------------- /src/config/logging.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from pydantic_settings import BaseSettings 4 | 5 | 6 | class Settings(BaseSettings): 7 | logging_on: bool = Field(default=True, alias="LOGGING_ON") 8 | logging_level: str = Field(default="DEBUG", alias="LOGGING_LEVEL") 9 | logging_json: bool = Field(default=True, alias="LOGGING_JSON") 10 | 11 | @property 12 | def log_config(self) -> dict: 13 | config = { 14 | "loggers": { 15 | "uvicorn": {"handlers": ["default"], "level": self.logging_level, "propagate": False}, 16 | "sqlalchemy": {"handlers": ["default"], "level": self.logging_level, "propagate": False}, 17 | } 18 | } 19 | return config 20 | 21 | 22 | def make_logger_conf(*confs, log_level, json_log): 23 | fmt = "%(asctime)s.%(msecs)03d [%(levelname)s]|[%(name)s]: %(message)s" 24 | datefmt = "%Y-%m-%d %H:%M:%S" 25 | config = { 26 | "version": 1, 27 | "disable_existing_loggers": True, 28 | "formatters": { 29 | "default": { 30 | "format": fmt, 31 | "datefmt": datefmt, 32 | }, 33 | "json": {"format": fmt, "datefmt": datefmt, "class": "pythonjsonlogger.jsonlogger.JsonFormatter"}, 34 | }, 35 | "handlers": { 36 | "default": { 37 | "level": log_level, 38 | "formatter": "json" if json_log else "default", 39 | "class": "logging.StreamHandler", 40 | "stream": "ext://sys.stdout", 41 | }, 42 | }, 43 | "loggers": { 44 | "": {"handlers": ["default"], "level": log_level, "propagate": False}, 45 | }, 46 | } 47 | for conf in confs: 48 | for key in conf.keys(): 49 | config[key].update(conf[key]) 50 | 51 | return config 52 | 53 | 54 | settings = Settings() 55 | logger_config = make_logger_conf( 56 | settings.log_config, 57 | log_level=settings.logging_level, 58 | json_log=settings.logging_json 59 | ) 60 | -------------------------------------------------------------------------------- /src/config/project.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | from pydantic import Field 3 | 4 | 5 | class Settings(BaseSettings): 6 | host: str = Field(alias="APP_HOST") 7 | port: int = Field(alias="APP_PORT") 8 | debug: bool = Field(default=False, alias="APP_DEBUG") 9 | version: str = Field(alias="APP_VERSION") 10 | hooks_enabled: bool = Field(default=True, alias="APP_HOOKS_ENABLED") 11 | root_path: str = Field(default="", alias="APP_ROOT_PATH") 12 | timezone_shift: int = Field(default=3, alias="APP_TIMEZONE_SHIFT") 13 | 14 | 15 | settings = Settings() 16 | -------------------------------------------------------------------------------- /src/config/security.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Settings(BaseSettings): 6 | secret_key: str = Field(..., alias="SECRET_KEY") 7 | access_token_expire_minutes: int = Field(..., alias="ACCESS_TOKEN_EXPIRE_MINUTES") 8 | algorithm: str = Field("HS256", alias="SECRET_KEY_ALGORITHM") 9 | 10 | 11 | settings = Settings() 12 | -------------------------------------------------------------------------------- /src/config/swagger.py: -------------------------------------------------------------------------------- 1 | from pydantic import ( 2 | Field, 3 | EmailStr, 4 | AnyUrl, 5 | ) 6 | 7 | from pydantic_settings import BaseSettings 8 | 9 | 10 | class Settings(BaseSettings): 11 | title: str = Field(default="СУП", alias="APP_TITLE") 12 | description: str | None = Field(default=None, alias="APP_DESCRIPTION") 13 | summary: str | None = Field(None, alias="APP_SUMMARY") 14 | terms_of_service: str | None = Field(None, alias="APP_TERMS_OF_SERVICE") 15 | licence_name: str = Field("Apache 2.0", alias="APP_LICENSE_NAME") 16 | licence_identifier: str = Field("MIT", alias="APP_LICENSE_IDENTIFIER") 17 | licence_url: AnyUrl | None = Field("https://www.apache.org/licenses/LICENSE-2.0.html", alias="APP_LICENSE_URL") 18 | contact_name: str | None = Field("Nikita Popov", alias="APP_CONTACT_NAME") 19 | contact_url: AnyUrl | None = Field("https://t.me/nikitqa47", alias="APP_CONTACT_URL") 20 | contact_email: EmailStr | None = Field("nikitqaa1901@gmail.com", alias="APP_CONTACT_EMAIL") 21 | docs_url: str | None = Field(None, alias="APP_DOCS_URL") 22 | redoc_url: str | None = Field(None, alias="APP_REDOC_URL") # noqa 23 | 24 | @property 25 | def contact(self) -> dict: 26 | return { 27 | "name": self.contact_name, 28 | "url": self.contact_url, 29 | "email": self.contact_email 30 | } 31 | 32 | @property 33 | def license(self) -> dict: 34 | return { 35 | "name": self.licence_name, 36 | "url": self.licence_url, 37 | "identifier": self.licence_identifier 38 | } 39 | 40 | 41 | settings = Settings() -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/lib/__init__.py -------------------------------------------------------------------------------- /src/lib/base_model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import TIMESTAMP, func 4 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 5 | 6 | 7 | class Base(DeclarativeBase): 8 | """Базовая модель SqlAlchemy""" 9 | __abstract__ = True 10 | 11 | id: Mapped[int] = mapped_column(primary_key=True, unique=True) 12 | created_at: Mapped[datetime] = mapped_column( 13 | TIMESTAMP(timezone=True), 14 | server_default=func.now() 15 | ) 16 | updated_at: Mapped[datetime] = mapped_column( 17 | TIMESTAMP(timezone=True), 18 | server_default=func.now(), 19 | onupdate=func.now() 20 | ) 21 | -------------------------------------------------------------------------------- /src/lib/dtos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJWOMS/sup/ac44b45813d112f448a4fdadc32f108395dec79a/src/lib/dtos/__init__.py -------------------------------------------------------------------------------- /src/lib/dtos/base_dto.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class BaseDto(BaseModel): 5 | """Базовая схема Pydantic""" 6 | class Config: 7 | from_attributes = True 8 | -------------------------------------------------------------------------------- /src/lib/exceptions.py: -------------------------------------------------------------------------------- 1 | class InviteError(Exception): 2 | pass 3 | 4 | 5 | class LoginError(Exception): 6 | pass 7 | 8 | 9 | class RegistrationError(Exception): 10 | pass 11 | 12 | 13 | class NotFoundError(Exception): 14 | pass 15 | 16 | 17 | class AlreadyExistError(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from src.app import get_application 3 | from src.config.project import settings as main_settings 4 | 5 | 6 | app = get_application() 7 | 8 | 9 | if __name__ == "__main__": 10 | uvicorn.run("main:app", host=main_settings.host, port=main_settings.port, reload=main_settings.debug) 11 | -------------------------------------------------------------------------------- /src/middleware.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from src.config.cors import settings as cors_settings 4 | 5 | 6 | def init_middleware(app: FastAPI): 7 | app.add_middleware( 8 | CORSMiddleware, 9 | allow_origins=cors_settings.allow_origins, 10 | allow_credentials=cors_settings.allow_credentials, 11 | allow_methods=cors_settings.allow_methods, 12 | allow_headers=cors_settings.allow_headers, 13 | allow_origin_regex=cors_settings.allow_origin_regex, 14 | max_age=cors_settings.max_age, 15 | ) 16 | -------------------------------------------------------------------------------- /src/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from src.api.v1.routes import router as v1 3 | 4 | 5 | def get_apps_router(): 6 | router = APIRouter() 7 | router.include_router(v1) 8 | return router 9 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MIGRATIONS_PATH="/app/migrations/versions" 4 | 5 | if [ "$DB_RUN_AUTO_MIGRATE" = "True" ]; then 6 | if [ -d "$MIGRATIONS_PATH" ] && [ "$(ls -A $MIGRATIONS_PATH)" ]; then 7 | echo "Running Alembic migrations..." 8 | alembic upgrade head 9 | else 10 | echo "No migration files found in $MIGRATIONS_PATH. Skipping migrations." 11 | fi 12 | else 13 | echo "Skipping Alembic migrations due to DB_RUN_AUTO_MIGRATE flag." 14 | fi 15 | 16 | uvicorn src.main:app --host "0.0.0.0" --port "8000" --log-level "debug" --reload --use-colors 17 | --------------------------------------------------------------------------------