diff --git a/api/app/controllers/register_router.py b/api/app/controllers/register_router.py new file mode 100644 index 0000000..118a607 --- /dev/null +++ b/api/app/controllers/register_router.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, Response +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database.session import get_db +from app.domain.entities.users import UserRead, UserRegister, UserCreate +from app.infrastructure.dependencies import require_admin +from app.infrastructure.register_service import RegisterService + +register_router = APIRouter() + + +@register_router.post( + '/register/', + response_model=UserRead, + summary='User registration', + description='Performs user registration in the system', +) +async def register_user( + user_data: UserRegister, + db: AsyncSession = Depends(get_db) +): + register_service = RegisterService(db) + return await register_service.user_register(user_data) + + +@register_router.post( + '/create/', + response_model=UserRead, + summary='User creation', + description='Performs user creation in the system', +) +async def create_user( + user_data: UserCreate, + user=Depends(require_admin), + db: AsyncSession = Depends(get_db) +): + register_service = RegisterService(db) + return await register_service.create_user(user_data) diff --git a/api/app/database/alembic/versions/6241a16321b4_0001_инициализация.py b/api/app/database/alembic/versions/6241a16321b4_0001_инициализация.py new file mode 100644 index 0000000..ae5a85b --- /dev/null +++ b/api/app/database/alembic/versions/6241a16321b4_0001_инициализация.py @@ -0,0 +1,196 @@ +"""0001 инициализация + +Revision ID: 6241a16321b4 +Revises: +Create Date: 2025-11-27 13:33:22.506743 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '6241a16321b4' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('courses', + sa.Column('title', sa.String(length=250), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('photo_filename', sa.String(), nullable=False), + sa.Column('photo_path', sa.String(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('roles', + sa.Column('title', sa.String(length=150), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('title'), + schema='public' + ) + op.create_table('statuses', + sa.Column('title', sa.String(length=250), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('title'), + schema='public' + ) + op.create_table('users', + sa.Column('first_name', sa.String(length=250), nullable=False), + sa.Column('last_name', sa.String(length=250), nullable=False), + sa.Column('patronymic', sa.String(length=250), nullable=True), + sa.Column('login', sa.String(length=250), nullable=False), + sa.Column('password_hash', sa.String(), nullable=False), + sa.Column('email', sa.String(length=250), nullable=True), + sa.Column('birthdate', sa.Date(), nullable=False), + sa.Column('reg_date', sa.Date(), nullable=False), + sa.Column('last_visit', sa.DateTime(), nullable=True), + sa.Column('photo_filename', sa.String(length=250), nullable=True), + sa.Column('photo_path', sa.String(), nullable=True), + sa.Column('status_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['role_id'], ['public.roles.id'], ), + sa.ForeignKeyConstraint(['status_id'], ['public.statuses.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('login'), + schema='public' + ) + op.create_table('course_teachers', + sa.Column('course_id', sa.Integer(), nullable=False), + sa.Column('teacher_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['course_id'], ['public.courses.id'], ), + sa.ForeignKeyConstraint(['teacher_id'], ['public.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('enrollments', + sa.Column('enrollment_date', sa.DateTime(), nullable=False), + sa.Column('course_id', sa.Integer(), nullable=False), + sa.Column('student_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['course_id'], ['public.courses.id'], ), + sa.ForeignKeyConstraint(['student_id'], ['public.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('lessons', + sa.Column('title', sa.String(length=250), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('text', sa.String(), nullable=True), + sa.Column('number', sa.Integer(), nullable=False), + sa.Column('course_id', sa.Integer(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['course_id'], ['public.courses.id'], ), + sa.ForeignKeyConstraint(['creator_id'], ['public.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('tasks', + sa.Column('title', sa.String(length=250), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('text', sa.String(), nullable=True), + sa.Column('number', sa.Integer(), nullable=False), + sa.Column('course_id', sa.Integer(), nullable=False), + sa.Column('creator_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['course_id'], ['public.courses.id'], ), + sa.ForeignKeyConstraint(['creator_id'], ['public.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('lesson_files', + sa.Column('lesson_id', sa.Integer(), nullable=False), + sa.Column('filename', sa.String(), nullable=False), + sa.Column('file_path', sa.String(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['lesson_id'], ['public.lessons.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('solutions', + sa.Column('answer_text', sa.String(), nullable=True), + sa.Column('assessment_text', sa.String(length=50), nullable=True), + sa.Column('assessment_autor_id', sa.Integer(), nullable=True), + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('student_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['assessment_autor_id'], ['public.users.id'], ), + sa.ForeignKeyConstraint(['student_id'], ['public.users.id'], ), + sa.ForeignKeyConstraint(['task_id'], ['public.tasks.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('task_files', + sa.Column('task_id', sa.Integer(), nullable=False), + sa.Column('filename', sa.String(), nullable=False), + sa.Column('file_path', sa.String(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['task_id'], ['public.tasks.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + op.create_table('solution_files', + sa.Column('solution_id', sa.Integer(), nullable=False), + sa.Column('filename', sa.String(), nullable=False), + sa.Column('file_path', sa.String(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['solution_id'], ['public.solutions.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('solution_files', schema='public') + op.drop_table('task_files', schema='public') + op.drop_table('solutions', schema='public') + op.drop_table('lesson_files', schema='public') + op.drop_table('tasks', schema='public') + op.drop_table('lessons', schema='public') + op.drop_table('enrollments', schema='public') + op.drop_table('course_teachers', schema='public') + op.drop_table('users', schema='public') + op.drop_table('statuses', schema='public') + op.drop_table('roles', schema='public') + op.drop_table('courses', schema='public') + # ### end Alembic commands ### diff --git a/api/app/database/alembic/versions/7a6554b361e8_0001_инициализация.py b/api/app/database/alembic/versions/7a6554b361e8_0001_инициализация.py deleted file mode 100644 index 8a1b7c1..0000000 --- a/api/app/database/alembic/versions/7a6554b361e8_0001_инициализация.py +++ /dev/null @@ -1,32 +0,0 @@ -"""0001 инициализация - -Revision ID: 7a6554b361e8 -Revises: -Create Date: 2025-11-26 19:52:23.751193 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '7a6554b361e8' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/api/app/domain/entities/users.py b/api/app/domain/entities/users.py index 30d71f1..9a2db5f 100644 --- a/api/app/domain/entities/users.py +++ b/api/app/domain/entities/users.py @@ -4,7 +4,7 @@ from typing import Optional from pydantic import BaseModel, EmailStr, Field -class UserCreate(BaseModel): +class UserRegister(BaseModel): first_name: str = Field(max_length=250) last_name: str = Field(max_length=250) patronymic: Optional[str] = Field(default=None, max_length=250) @@ -12,11 +12,15 @@ class UserCreate(BaseModel): email: Optional[EmailStr] = None birthdate: date password: str = Field(min_length=8) - role_id: Optional[int] = Field(default=None) repeat_password: str = Field(min_length=8) +class UserCreate(UserRegister): + role_id: int = Field() + + class UserRead(BaseModel): + id: int first_name: str last_name: str patronymic: Optional[str] @@ -28,3 +32,4 @@ class UserRead(BaseModel): class Config: orm_mode = True + from_attributes = True diff --git a/api/app/domain/models/__init__.py b/api/app/domain/models/__init__.py index 2f9e42e..9fc41d2 100644 --- a/api/app/domain/models/__init__.py +++ b/api/app/domain/models/__init__.py @@ -8,3 +8,16 @@ metadata_obj = MetaData(schema=Settings().db_schema) class Base(DeclarativeBase): metadata = metadata_obj + +from app.domain.models.course_teachers import CourseTeacher +from app.domain.models.courses import Course +from app.domain.models.enrollments import Enrollment +from app.domain.models.lesson_files import LessonFile +from app.domain.models.lessons import Lesson +from app.domain.models.roles import Role +from app.domain.models.solution_files import SolutionFile +from app.domain.models.solutions import Solution +from app.domain.models.statuses import Status +from app.domain.models.task_files import TaskFile +from app.domain.models.tasks import Task +from app.domain.models.users import User diff --git a/api/app/domain/models/base.py b/api/app/domain/models/base.py index afe85a8..33b76b9 100644 --- a/api/app/domain/models/base.py +++ b/api/app/domain/models/base.py @@ -9,9 +9,9 @@ from app.domain.models import Base class RootTable(Base): __abstract__ = True - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False) - created_at: Mapped[datetime] = mapped_column(default=func.now()) - updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now()) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + created_at: Mapped[datetime] = mapped_column(server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(onupdate=func.now(), server_default=func.now()) class PhotoAbstract(RootTable): diff --git a/api/app/domain/models/courses.py b/api/app/domain/models/courses.py index 74fa646..113e150 100644 --- a/api/app/domain/models/courses.py +++ b/api/app/domain/models/courses.py @@ -10,7 +10,7 @@ class Course(PhotoAbstract): __tablename__ = 'courses' title: Mapped[str] = mapped_column(String(250), nullable=False) - description: Mapped[str] = mapped_column(String(1000)) + description: Mapped[str] = mapped_column(String(1000), nullable=True) teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course') enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course') diff --git a/api/app/domain/models/lessons.py b/api/app/domain/models/lessons.py index e00028c..2393bb1 100644 --- a/api/app/domain/models/lessons.py +++ b/api/app/domain/models/lessons.py @@ -10,8 +10,8 @@ class Lesson(RootTable): __tablename__ = 'lessons' title: Mapped[str] = mapped_column(String(250), nullable=False) - description: Mapped[str] = mapped_column() - text: Mapped[str] = mapped_column() + description: Mapped[str] = mapped_column(nullable=True) + text: Mapped[str] = mapped_column(nullable=True) number: Mapped[int] = mapped_column(nullable=False) course_id: Mapped[int] = mapped_column(ForeignKey('courses.id'), nullable=False) @@ -20,4 +20,4 @@ class Lesson(RootTable): course: Mapped['Course'] = relationship('Course', back_populates='lessons') creator: Mapped['User'] = relationship('User', back_populates='created_lessons') - files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lessons') + files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lesson') diff --git a/api/app/domain/models/solutions.py b/api/app/domain/models/solutions.py index 1c5234e..6dbafe8 100644 --- a/api/app/domain/models/solutions.py +++ b/api/app/domain/models/solutions.py @@ -9,10 +9,10 @@ from app.domain.models.base import RootTable class Solution(RootTable): __tablename__ = 'solutions' - answer_text: Mapped[str] = mapped_column() - assessment_text: Mapped[str] = mapped_column(String(50)) + answer_text: Mapped[str] = mapped_column(nullable=True) + assessment_text: Mapped[str] = mapped_column(String(50), nullable=True) - assessment_autor_id: Mapped[int] = mapped_column(ForeignKey('users.id')) + assessment_autor_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True) task_id: Mapped[int] = mapped_column(ForeignKey('tasks.id'), nullable=False) student_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) diff --git a/api/app/domain/models/statuses.py b/api/app/domain/models/statuses.py index 1706cb1..a60a73c 100644 --- a/api/app/domain/models/statuses.py +++ b/api/app/domain/models/statuses.py @@ -11,4 +11,4 @@ class Status(RootTable): title: Mapped[str] = mapped_column(String(250), nullable=False, unique=True) - users = Mapped[List['User']] = relationship('User', back_populates='status') + users: Mapped[List['User']] = relationship('User', back_populates='status') diff --git a/api/app/domain/models/tasks.py b/api/app/domain/models/tasks.py index 5a0fd5c..7f1446d 100644 --- a/api/app/domain/models/tasks.py +++ b/api/app/domain/models/tasks.py @@ -10,8 +10,8 @@ class Task(RootTable): __tablename__ = 'tasks' title: Mapped[str] = mapped_column(String(250), nullable=False) - description: Mapped[str] = mapped_column() - text: Mapped[str] = mapped_column() + description: Mapped[str] = mapped_column(nullable=True) + text: Mapped[str] = mapped_column(nullable=True) number: Mapped[int] = mapped_column(nullable=False) course_id: Mapped[int] = mapped_column(ForeignKey('courses.id'), nullable=False) @@ -20,4 +20,4 @@ class Task(RootTable): course: Mapped['Course'] = relationship('Course', back_populates='tasks') creator: Mapped['User'] = relationship('User', back_populates='created_tasks') - files: Mapped[List['TaskFile']] = relationship('TaskFile', back_populates='lessons') + files: Mapped[List['TaskFile']] = relationship('TaskFile', back_populates='task') diff --git a/api/app/domain/models/users.py b/api/app/domain/models/users.py index 39dba6b..d80214c 100644 --- a/api/app/domain/models/users.py +++ b/api/app/domain/models/users.py @@ -14,15 +14,15 @@ class User(PhotoAbstract): first_name: Mapped[str] = mapped_column(String(250), nullable=False) last_name: Mapped[str] = mapped_column(String(250), nullable=False) - patronymic: Mapped[str] = mapped_column(String(250)) + patronymic: Mapped[str] = mapped_column(String(250), nullable=True) login: Mapped[str] = mapped_column(String(250), nullable=False, unique=True) password_hash: Mapped[str] = mapped_column(nullable=False) - email: Mapped[str] = mapped_column(String(250), unique=True) + email: Mapped[str] = mapped_column(String(250), unique=True, nullable=True) birthdate: Mapped[date] = mapped_column(nullable=False) reg_date: Mapped[date] = mapped_column(nullable=False, default=func.now()) - last_visit: Mapped[datetime] = mapped_column() - photo_filename: Mapped[str] = mapped_column(String(250)) - photo_path: Mapped[str] = mapped_column() + last_visit: Mapped[datetime] = mapped_column(nullable=True) + photo_filename: Mapped[str] = mapped_column(String(250), nullable=True) + photo_path: Mapped[str] = mapped_column(nullable=True) status_id: Mapped[int] = mapped_column(ForeignKey('statuses.id'), nullable=False) role_id: Mapped[int] = mapped_column(ForeignKey('roles.id'), nullable=False) @@ -34,15 +34,17 @@ class User(PhotoAbstract): enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='student') created_lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='creator') created_tasks: Mapped[List['Task']] = relationship('Task', back_populates='creator') + + from app.domain.models.solutions import Solution assessments: Mapped[List['Solution']] = relationship( 'Solution', back_populates='assessment_autor', - foreign_keys=['assessment_autor_id'], + foreign_keys=[Solution.assessment_autor_id], ) my_solutions: Mapped[List['Solution']] = relationship( 'Solution', back_populates='student', - foreign_keys=['student_id'], + foreign_keys=[Solution.student_id], ) def check_password(self, password): diff --git a/api/app/infrastructure/register_service.py b/api/app/infrastructure/register_service.py new file mode 100644 index 0000000..34d3075 --- /dev/null +++ b/api/app/infrastructure/register_service.py @@ -0,0 +1,144 @@ +import re +from typing import Optional + +from fastapi import HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.application.roles_repository import RolesRepository +from app.application.statuses_repository import StatusesRepository +from app.application.users_repository import UsersRepository +from app.core.constants import UserRoles +from app.domain.entities.users import UserRead, UserCreate, UserRegister +from app.domain.models.users import User +from app.settings import Settings + + +class RegisterService: + def __init__(self, db: AsyncSession): + self.users_repository = UsersRepository(db) + self.roles_repository = RolesRepository(db) + self.statuses_repository = StatusesRepository(db) + self.settings = Settings() + + async def create_user(self, create_user_entity: UserCreate) -> Optional[UserRead]: + user = await self.users_repository.get_by_login(create_user_entity.login) + if user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пользователь с таким логином уже существует', + ) + + if create_user_entity.password != create_user_entity.repeat_password: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пароли не совпадают', + ) + + if not self.is_strong_password(create_user_entity.repeat_password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пароль слишком слабый. Пароль должен содержать не менее 8 символов, включая хотя бы одну букву и одну цифру и один специальный символ.' + ) + + default_status = await self.statuses_repository.get_by_title(self.settings.default_status) + if default_status is None: + raise HTTPException( + status_code=status.HTTP_424_FAILED_DEPENDENCY, + detail='Статус по умолчанию не найден', + ) + + user = User( + first_name=create_user_entity.first_name, + last_name=create_user_entity.last_name, + patronymic=create_user_entity.patronymic, + login=create_user_entity.login, + email=create_user_entity.email, + birthdate=create_user_entity.birthdate, + status_id=default_status.id, + ) + + role = await self.roles_repository.get_by_id(create_user_entity.role_id) + if role is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Роль с таким ID не найдена', + ) + + user.role_id = role.id + + user.set_password(create_user_entity.password) + + user = await self.users_repository.create(user) + + return UserRead.model_validate(user) + + async def user_register(self, register_user_entity: UserRegister) -> UserRead: + user = await self.users_repository.get_by_login(register_user_entity.login) + if user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пользователь с таким логином уже существует', + ) + + if register_user_entity.password != register_user_entity.repeat_password: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пароли не совпадают', + ) + + if not self.is_strong_password(register_user_entity.repeat_password): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Пароль слишком слабый. Пароль должен содержать не менее 8 символов, включая хотя бы одну букву и одну цифру и один специальный символ.' + ) + + default_status = await self.statuses_repository.get_by_title(self.settings.default_status) + if default_status is None: + raise HTTPException( + status_code=status.HTTP_424_FAILED_DEPENDENCY, + detail='Статус по умолчанию не найден', + ) + + user = User( + first_name=register_user_entity.first_name, + last_name=register_user_entity.last_name, + patronymic=register_user_entity.patronymic, + login=register_user_entity.login, + email=register_user_entity.email, + birthdate=register_user_entity.birthdate, + status_id=default_status.id, + ) + + role = await self.roles_repository.get_by_title(self.settings.default_role_name) + if role is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Роль по умолчанию не найдена', + ) + + user.role_id = role.id + + user.set_password(register_user_entity.password) + + user = await self.users_repository.create(user) + + return UserRead.model_validate(user) + + @staticmethod + def is_strong_password(password: str) -> bool: + if len(password) < 8: + return False + + if not re.search(r'[A-Z]', password): + return False + + if not re.search(r'[a-z]', password): + return False + + if not re.search(r'\d', password): + return False + + if not re.search(r'[!@#$%^&*(),.?\':{}|<>]', password): + return False + + return True diff --git a/api/app/infrastructure/users_service.py b/api/app/infrastructure/users_service.py index ef5928e..903895b 100644 --- a/api/app/infrastructure/users_service.py +++ b/api/app/infrastructure/users_service.py @@ -7,96 +7,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.application.roles_repository import RolesRepository from app.application.statuses_repository import StatusesRepository from app.application.users_repository import UsersRepository -from app.domain.entities.users import UserCreate, UserRead +from app.domain.entities.users import UserRegister, UserRead from app.domain.models.users import User from app.settings import Settings class UsersService: def __init__(self, db: AsyncSession): - self.users_repository = UsersRepository(db) - self.roles_repository = RolesRepository(db) - self.statuses_repository = StatusesRepository(db) - self.settings = Settings() - - async def create_user(self, create_user_entity: UserCreate) -> Optional[UserRead]: - user = await self.users_repository.get_by_login(create_user_entity.login) - if user: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пользователь с таким логином уже существует', - ) - - if create_user_entity.password != create_user_entity.repeat_password: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пароли не совпадают', - ) - - if not self.is_strong_password(create_user_entity.repeat_password): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Пароль слишком слабый. Пароль должен содержать не менее 8 символов, включая хотя бы одну букву и одну цифру и один специальный символ.' - ) - - default_status = await self.statuses_repository.get_by_title(self.settings.default_status) - if default_status is None: - raise HTTPException( - status_code=status.HTTP_424_FAILED_DEPENDENCY, - detail='Статус по умолчанию не найден', - ) - - user = User( - first_name=create_user_entity.first_name, - last_name=create_user_entity.last_name, - patronymic=create_user_entity.patronymic, - login=create_user_entity.login, - email=create_user_entity.email, - birthdate=create_user_entity.birthdate, - status_id=default_status.status_id, - ) - - if create_user_entity.role_id is None: - default_role = await self.roles_repository.get_by_title(self.settings.default_role_name) - if default_role is None: - raise HTTPException( - status_code=status.HTTP_424_FAILED_DEPENDENCY, - detail='Роль по умолчанию не найдена', - ) - - user.role_id = default_role.id - - else: - role = await self.roles_repository.get_by_id(create_user_entity.role_id) - if role is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Роль с таким ID не найдена', - ) - - user.role_id = role.id - - user.set_password(create_user_entity.password) - - user = await self.users_repository.create(user) - - return UserRead.model_validate(user) - - @staticmethod - def is_strong_password(password: str) -> bool: - if len(password) < 8: - return False - - if not re.search(r'[A-Z]', password): - return False - - if not re.search(r'[a-z]', password): - return False - - if not re.search(r'\d', password): - return False - - if not re.search(r'[!@#$%^&*(),.?\':{}|<>]', password): - return False - - return True + pass diff --git a/api/app/main.py b/api/app/main.py index 9fbb32a..b7d548d 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -2,6 +2,7 @@ from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from app.controllers.auth_router import auth_router +from app.controllers.register_router import register_router from app.settings import Settings @@ -18,6 +19,7 @@ def start_app(): ) api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth']) + api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register']) return api_app diff --git a/api/req.txt b/api/req.txt index ee7f794..1529607 100644 --- a/api/req.txt +++ b/api/req.txt @@ -5,4 +5,5 @@ asyncpg==0.31.0 greenlet==3.2.4 werkzeug==3.1.3 pyjwt==2.9.0 -fastapi==0.115.0 \ No newline at end of file +fastapi==0.115.0 +pydantic[email]==2.11.4 \ No newline at end of file