сделал регистрацию, изменил модели данных

This commit is contained in:
Андрей Дувакин 2025-11-27 13:38:12 +05:00
parent 7d5cbdf431
commit 408325a277
16 changed files with 427 additions and 143 deletions

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ from typing import Optional
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel): class UserRegister(BaseModel):
first_name: str = Field(max_length=250) first_name: str = Field(max_length=250)
last_name: str = Field(max_length=250) last_name: str = Field(max_length=250)
patronymic: Optional[str] = Field(default=None, max_length=250) patronymic: Optional[str] = Field(default=None, max_length=250)
@ -12,11 +12,15 @@ class UserCreate(BaseModel):
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
birthdate: date birthdate: date
password: str = Field(min_length=8) password: str = Field(min_length=8)
role_id: Optional[int] = Field(default=None)
repeat_password: str = Field(min_length=8) repeat_password: str = Field(min_length=8)
class UserCreate(UserRegister):
role_id: int = Field()
class UserRead(BaseModel): class UserRead(BaseModel):
id: int
first_name: str first_name: str
last_name: str last_name: str
patronymic: Optional[str] patronymic: Optional[str]
@ -28,3 +32,4 @@ class UserRead(BaseModel):
class Config: class Config:
orm_mode = True orm_mode = True
from_attributes = True

View File

@ -8,3 +8,16 @@ metadata_obj = MetaData(schema=Settings().db_schema)
class Base(DeclarativeBase): class Base(DeclarativeBase):
metadata = metadata_obj 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

View File

@ -9,9 +9,9 @@ from app.domain.models import Base
class RootTable(Base): class RootTable(Base):
__abstract__ = True __abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(default=func.now()) created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now()) updated_at: Mapped[datetime] = mapped_column(onupdate=func.now(), server_default=func.now())
class PhotoAbstract(RootTable): class PhotoAbstract(RootTable):

View File

@ -10,7 +10,7 @@ class Course(PhotoAbstract):
__tablename__ = 'courses' __tablename__ = 'courses'
title: Mapped[str] = mapped_column(String(250), nullable=False) 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') teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course')
enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course') enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course')

View File

@ -10,8 +10,8 @@ class Lesson(RootTable):
__tablename__ = 'lessons' __tablename__ = 'lessons'
title: Mapped[str] = mapped_column(String(250), nullable=False) title: Mapped[str] = mapped_column(String(250), nullable=False)
description: Mapped[str] = mapped_column() description: Mapped[str] = mapped_column(nullable=True)
text: Mapped[str] = mapped_column() text: Mapped[str] = mapped_column(nullable=True)
number: Mapped[int] = mapped_column(nullable=False) number: Mapped[int] = mapped_column(nullable=False)
course_id: Mapped[int] = mapped_column(ForeignKey('courses.id'), 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') course: Mapped['Course'] = relationship('Course', back_populates='lessons')
creator: Mapped['User'] = relationship('User', back_populates='created_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')

View File

@ -9,10 +9,10 @@ from app.domain.models.base import RootTable
class Solution(RootTable): class Solution(RootTable):
__tablename__ = 'solutions' __tablename__ = 'solutions'
answer_text: Mapped[str] = mapped_column() answer_text: Mapped[str] = mapped_column(nullable=True)
assessment_text: Mapped[str] = mapped_column(String(50)) 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) task_id: Mapped[int] = mapped_column(ForeignKey('tasks.id'), nullable=False)
student_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) student_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)

View File

@ -11,4 +11,4 @@ class Status(RootTable):
title: Mapped[str] = mapped_column(String(250), nullable=False, unique=True) 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')

View File

@ -10,8 +10,8 @@ class Task(RootTable):
__tablename__ = 'tasks' __tablename__ = 'tasks'
title: Mapped[str] = mapped_column(String(250), nullable=False) title: Mapped[str] = mapped_column(String(250), nullable=False)
description: Mapped[str] = mapped_column() description: Mapped[str] = mapped_column(nullable=True)
text: Mapped[str] = mapped_column() text: Mapped[str] = mapped_column(nullable=True)
number: Mapped[int] = mapped_column(nullable=False) number: Mapped[int] = mapped_column(nullable=False)
course_id: Mapped[int] = mapped_column(ForeignKey('courses.id'), 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') course: Mapped['Course'] = relationship('Course', back_populates='tasks')
creator: Mapped['User'] = relationship('User', back_populates='created_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')

View File

@ -14,15 +14,15 @@ class User(PhotoAbstract):
first_name: Mapped[str] = mapped_column(String(250), nullable=False) first_name: Mapped[str] = mapped_column(String(250), nullable=False)
last_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) login: Mapped[str] = mapped_column(String(250), nullable=False, unique=True)
password_hash: Mapped[str] = mapped_column(nullable=False) 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) birthdate: Mapped[date] = mapped_column(nullable=False)
reg_date: Mapped[date] = mapped_column(nullable=False, default=func.now()) reg_date: Mapped[date] = mapped_column(nullable=False, default=func.now())
last_visit: Mapped[datetime] = mapped_column() last_visit: Mapped[datetime] = mapped_column(nullable=True)
photo_filename: Mapped[str] = mapped_column(String(250)) photo_filename: Mapped[str] = mapped_column(String(250), nullable=True)
photo_path: Mapped[str] = mapped_column() photo_path: Mapped[str] = mapped_column(nullable=True)
status_id: Mapped[int] = mapped_column(ForeignKey('statuses.id'), nullable=False) status_id: Mapped[int] = mapped_column(ForeignKey('statuses.id'), nullable=False)
role_id: Mapped[int] = mapped_column(ForeignKey('roles.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') enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='student')
created_lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='creator') created_lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='creator')
created_tasks: Mapped[List['Task']] = relationship('Task', 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( assessments: Mapped[List['Solution']] = relationship(
'Solution', 'Solution',
back_populates='assessment_autor', back_populates='assessment_autor',
foreign_keys=['assessment_autor_id'], foreign_keys=[Solution.assessment_autor_id],
) )
my_solutions: Mapped[List['Solution']] = relationship( my_solutions: Mapped[List['Solution']] = relationship(
'Solution', 'Solution',
back_populates='student', back_populates='student',
foreign_keys=['student_id'], foreign_keys=[Solution.student_id],
) )
def check_password(self, password): def check_password(self, password):

View File

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

View File

@ -7,96 +7,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.application.roles_repository import RolesRepository from app.application.roles_repository import RolesRepository
from app.application.statuses_repository import StatusesRepository from app.application.statuses_repository import StatusesRepository
from app.application.users_repository import UsersRepository 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.domain.models.users import User
from app.settings import Settings from app.settings import Settings
class UsersService: class UsersService:
def __init__(self, db: AsyncSession): def __init__(self, db: AsyncSession):
self.users_repository = UsersRepository(db) pass
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

View File

@ -2,6 +2,7 @@ from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from app.controllers.auth_router import auth_router from app.controllers.auth_router import auth_router
from app.controllers.register_router import register_router
from app.settings import Settings 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(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 return api_app

View File

@ -5,4 +5,5 @@ asyncpg==0.31.0
greenlet==3.2.4 greenlet==3.2.4
werkzeug==3.1.3 werkzeug==3.1.3
pyjwt==2.9.0 pyjwt==2.9.0
fastapi==0.115.0 fastapi==0.115.0
pydantic[email]==2.11.4