Compare commits

...

10 Commits

61 changed files with 3907 additions and 12 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ API/.idea
API/.venv/
API/.idea/
API/.env

View File

@ -0,0 +1,32 @@
from typing import Optional, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import ProfilePhoto
class ProfilePhotosRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, photo_id: int) -> Optional[ProfilePhoto]:
stmt = select(ProfilePhoto).filter_by(id=photo_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def get_by_profile_id(self, profile_id: int) -> Sequence[ProfilePhoto]:
stmt = select(ProfilePhoto).filter_by(profile_id=profile_id)
result = await self.db.execute(stmt)
return result.scalars().all()
async def create(self, photo: ProfilePhoto) -> ProfilePhoto:
self.db.add(photo)
await self.db.commit()
await self.db.refresh(photo)
return photo
async def delete(self, photo: ProfilePhoto) -> ProfilePhoto:
await self.db.delete(photo)
await self.db.commit()
return photo

View File

@ -0,0 +1,32 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Profile
class ProfilesRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, profile_id: int) -> Optional[Profile]:
stmt = select(Profile).filter_by(id=profile_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def create(self, profile: Profile) -> Profile:
self.db.add(profile)
await self.db.commit()
await self.db.refresh(profile)
return profile
async def update(self, profile: Profile) -> Profile:
await self.db.merge(profile)
await self.db.commit()
return profile
async def delete(self, profile: Profile) -> Profile:
await self.db.delete(profile)
await self.db.commit()
return profile

View File

@ -0,0 +1,37 @@
from typing import Optional, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Project
class ProjectsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self) -> Sequence[Project]:
stmt = select(Project)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_id(self, project_id: int) -> Optional[Project]:
stmt = select(Project).filter_by(id=project_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def create(self, project: Project) -> Project:
self.db.add(project)
await self.db.commit()
await self.db.refresh(project)
return project
async def update(self, project: Project) -> Project:
await self.db.merge(project)
await self.db.commit()
return project
async def delete(self, project: Project) -> Project:
await self.db.delete(project)
await self.db.commit()
return project

View File

@ -0,0 +1,16 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Role
class RolesRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, role_id: int) -> Optional[Role]:
stmt = select(Role).filter_by(id=role_id)
result = await self.db.execute(stmt)
return result.scalars().first()

View File

@ -0,0 +1,37 @@
from typing import Optional, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Team
class TeamsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_all(self) -> Sequence[Team]:
stmt = select(Team)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_id(self, team_id: int) -> Optional[Team]:
stmt = select(Team).filter_by(id=team_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def create(self, team: Team) -> Team:
self.db.add(team)
await self.db.commit()
await self.db.refresh(team)
return team
async def update(self, team: Team) -> Team:
await self.db.merge(team)
await self.db.commit()
return team
async def delete(self, team: Team) -> Team:
await self.db.delete(team)
await self.db.commit()
return team

View File

@ -0,0 +1,47 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from typing_extensions import Optional
from app.domain.models import User, Profile
class UsersRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, user_id: int) -> Optional[User]:
stmt = select(User).filter_by(id=user_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def get_by_id_with_role(self, user_id: int) -> Optional[User]:
stmt = (
select(User)
.filter_by(id=user_id)
.options(
joinedload(User.profile).joinedload(Profile.role)
)
)
result = await self.db.execute(stmt)
return result.scalars().first()
async def get_by_login(self, login: str) -> Optional[User]:
stmt = (
select(User)
.filter_by(login=login)
.options(joinedload(User.profile))
)
result = await self.db.execute(stmt)
return result.scalars().first()
async def create(self, user: User) -> User:
self.db.add(user)
await self.db.commit()
await self.db.refresh(user)
return user
async def update(self, user: User) -> User:
await self.db.merge(user)
await self.db.commit()
return user

View File

@ -0,0 +1,33 @@
from fastapi import APIRouter, Response, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.auth import AuthEntity
from app.domain.entities.token_entity import TokenEntity
from app.infrastructure.auth_service import AuthService
router = APIRouter()
@router.post(
'/',
response_model=TokenEntity,
summary="User authentication",
description="Logs in the user and outputs the `access_token` in the `cookie'",
)
async def auth_user(
response: Response,
user_data: AuthEntity,
db: AsyncSession = Depends(get_db),
):
auth_service = AuthService(db)
token = await auth_service.authenticate_user(user_data.login, user_data.password)
response.set_cookie(
key="users_access_token",
value=token["access_token"],
httponly=True,
samesite="Lax",
)
return token

View File

@ -0,0 +1,69 @@
from fastapi import Depends, File, UploadFile, APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from app.database.session import get_db
from app.domain.entities.profile_photo import ProfilePhotoEntity
from app.infrastructure.dependencies import get_current_user, require_admin
from app.infrastructure.profile_photos_service import ProfilePhotosService
router = APIRouter()
@router.get(
"/profiles/{profile_id}/",
response_model=list[ProfilePhotoEntity],
summary="Get all photos metadata for a profile",
description="Returns metadata of all profile photos for the given profile_id",
)
async def get_photos_by_profile_id(
profile_id: int,
db: AsyncSession = Depends(get_db),
):
service = ProfilePhotosService(db)
return await service.get_photo_file_by_profile_id(profile_id)
@router.get(
"/{photo_id}/file",
response_class=FileResponse,
summary="Download photo file by photo ID",
description="Returns the image file for the given photo ID",
)
async def download_photo_file(
photo_id: int,
db: AsyncSession = Depends(get_db),
):
service = ProfilePhotosService(db)
return await service.get_photo_file_by_id(photo_id)
@router.post(
"/profiles/{profile_id}/upload",
response_model=ProfilePhotoEntity,
summary="Upload a new photo for a profile",
description="Uploads a new photo file and associates it with the given profile ID",
)
async def upload_photo(
profile_id: int,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProfilePhotosService(db)
return await service.upload_photo(profile_id, file, user)
@router.delete(
"/{photo_id}/",
response_model=ProfilePhotoEntity,
summary="Delete a photo by ID",
description="Deletes a photo and its file from storage",
)
async def delete_photo(
photo_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProfilePhotosService(db)
return await service.delete_photo(photo_id, user)

View File

@ -0,0 +1,57 @@
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.profile import ProfileEntity
from app.infrastructure.dependencies import get_current_user, require_admin
from app.infrastructure.profiles_service import ProfilesService
router = APIRouter()
@router.post(
'/',
response_model=Optional[ProfileEntity],
summary='Create a new profile',
description='Creates a new profile',
)
async def create_profile(
profile: ProfileEntity,
db: AsyncSession = Depends(get_db),
user=Depends(require_admin),
):
profiles_service = ProfilesService(db)
return await profiles_service.create_profile(profile)
@router.put(
'/{profile_id}/',
response_model=Optional[ProfileEntity],
summary='Update a profile',
description='Updates a profile',
)
async def update_profile(
profile_id: int,
profile: ProfileEntity,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
profiles_service = ProfilesService(db)
return await profiles_service.update_profile(profile_id, profile, user)
@router.delete(
'/{profile_id}/',
response_model=Optional[ProfileEntity],
summary='Delete a profile',
description='Delete a profile',
)
async def delete_profile(
profile_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(require_admin),
):
profiles_service = ProfilesService(db)
return await profiles_service.delete(profile_id, user)

View File

@ -0,0 +1,26 @@
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.register import RegisterEntity
from app.domain.entities.user import UserEntity
from app.infrastructure.users_service import UsersService
router = APIRouter()
@router.post(
'/',
response_model=Optional[UserEntity],
summary='User Registration',
description='Performs user registration in the system',
)
async def register_user(
user_data: RegisterEntity,
db: AsyncSession = Depends(get_db)
):
users_service = UsersService(db)
user = await users_service.register_user(user_data)
return user

View File

@ -0,0 +1,71 @@
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.team import TeamEntity
from app.infrastructure.dependencies import get_current_user, require_admin
from app.infrastructure.teams_service import TeamsService
router = APIRouter()
@router.get(
'/',
response_model=list[TeamEntity],
summary='Get all teams',
description='Returns all teams',
)
async def get_all_teams(
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
teams_service = TeamsService(db)
return await teams_service.get_all_teams()
@router.post(
'/',
response_model=Optional[TeamEntity],
summary='Create a new team',
description='Creates a new team',
)
async def create_team(
team: TeamEntity,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
teams_service = TeamsService(db)
return await teams_service.create_team(team)
@router.put(
'/{team_id}/',
response_model=Optional[TeamEntity],
summary='Update a team',
description='Updates a team',
)
async def update_team(
team_id: int,
team: TeamEntity,
db: AsyncSession = Depends(get_db),
user=Depends(require_admin),
):
teams_service = TeamsService(db)
return await teams_service.update_team(team_id, team)
@router.delete(
'/{team_id}/',
response_model=Optional[TeamEntity],
summary='Delete a team',
description='Delete a team',
)
async def delete_team(
team_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(require_admin),
):
teams_service = TeamsService(db)
return await teams_service.delete_team(team_id)

View File

@ -0,0 +1,27 @@
from typing import Optional
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.user import UserEntity
from app.infrastructure.dependencies import get_current_user
from app.infrastructure.users_service import UsersService
router = APIRouter()
@router.put(
'/{user_id}/',
response_model=Optional[UserEntity],
summary='Change user password',
description='Change user password',
)
async def create_user(
user_id: int,
new_password: str,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
users_service = UsersService(db)
return await users_service.change_user_password(user_id, new_password, user)

View File

@ -0,0 +1,32 @@
"""0002_добавил_поле_filename_уотографий_профиля
Revision ID: b4056dda0936
Revises: d53be3a35511
Create Date: 2025-05-05 21:06:32.080386
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b4056dda0936'
down_revision: Union[str, None] = 'd53be3a35511'
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.add_column('profile_photos', sa.Column('filename', sa.String(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('profile_photos', 'filename')
# ### end Alembic commands ###

View File

@ -0,0 +1,169 @@
"""0001_инициализация
Revision ID: d53be3a35511
Revises:
Create Date: 2025-04-13 13:16:29.461322
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = 'd53be3a35511'
down_revision: Union[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('contest_statuses',
sa.Column('title', sa.VARCHAR(length=150), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title')
)
op.create_table('projects',
sa.Column('description', sa.VARCHAR(length=150), nullable=True),
sa.Column('repository_url', sa.String(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('roles',
sa.Column('title', mysql.VARCHAR(length=150), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title')
)
op.create_table('teams',
sa.Column('title', sa.VARCHAR(length=150), nullable=False),
sa.Column('description', sa.VARCHAR(length=150), nullable=True),
sa.Column('logo', sa.String(), nullable=True),
sa.Column('git_url', sa.String(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('contests',
sa.Column('title', sa.VARCHAR(length=150), nullable=False),
sa.Column('description', sa.String(), nullable=True),
sa.Column('web_url', sa.String(), nullable=False),
sa.Column('photo', sa.String(), nullable=True),
sa.Column('results', sa.String(), nullable=True),
sa.Column('is_win', sa.Boolean(), nullable=True),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('status_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
sa.ForeignKeyConstraint(['status_id'], ['contest_statuses.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title')
)
op.create_table('profiles',
sa.Column('first_name', sa.VARCHAR(length=150), nullable=False),
sa.Column('last_name', sa.VARCHAR(length=150), nullable=False),
sa.Column('patronymic', sa.VARCHAR(length=150), nullable=True),
sa.Column('birthday', sa.Date(), nullable=False),
sa.Column('email', sa.VARCHAR(length=150), nullable=True),
sa.Column('phone', sa.VARCHAR(length=28), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('team_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('project_files',
sa.Column('file_path', sa.String(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('file_path')
)
op.create_table('contest_carousel_photos',
sa.Column('file_path', sa.String(), nullable=False),
sa.Column('number', sa.Integer(), nullable=False),
sa.Column('contest_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['contest_id'], ['contests.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('contest_files',
sa.Column('file_path', sa.String(), nullable=False),
sa.Column('contest_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['contest_id'], ['contests.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('profile_photos',
sa.Column('file_path', sa.String(), nullable=False),
sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('project_members',
sa.Column('description', sa.String(), nullable=True),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('users',
sa.Column('login', sa.VARCHAR(length=150), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('login')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('users')
op.drop_table('project_members')
op.drop_table('profile_photos')
op.drop_table('contest_files')
op.drop_table('contest_carousel_photos')
op.drop_table('project_files')
op.drop_table('profiles')
op.drop_table('contests')
op.drop_table('teams')
op.drop_table('roles')
op.drop_table('projects')
op.drop_table('contest_statuses')
# ### end Alembic commands ###

View File

@ -0,0 +1,15 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from app.settings import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session_maker = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)
async def get_db():
async with async_session_maker() as session:
yield session

View File

@ -0,0 +1,5 @@
from app.domain.entities.base_user import BaseUserEntity
class AuthEntity(BaseUserEntity):
pass

View File

@ -0,0 +1,19 @@
import datetime
from typing import Optional
from pydantic import BaseModel
class BaseProfileEntity(BaseModel):
first_name: str
last_name: str
patronymic: Optional[str] = None
birthday: datetime.date
email: Optional[str] = None
phone: Optional[str] = None
role_id: int
team_id: int
class Config:
abstract = True

View File

@ -0,0 +1,9 @@
from pydantic import BaseModel
class BaseUserEntity(BaseModel):
login: str
password: str
class Config:
abstract = True

View File

@ -0,0 +1,7 @@
from typing import Optional
from app.domain.entities.base_profile import BaseProfileEntity
class ProfileEntity(BaseProfileEntity):
id: Optional[int] = None

View File

@ -0,0 +1,8 @@
from pydantic import BaseModel
class ProfilePhotoEntity(BaseModel):
id: int
filename: str
file_path: str
profile_id: int

View File

@ -0,0 +1,9 @@
from typing import Optional
from pydantic import BaseModel
class ProjectEntity(BaseModel):
id: Optional[int] = None
description: str
repository_url: Optional[str] = None

View File

@ -0,0 +1,6 @@
from app.domain.entities.base_profile import BaseProfileEntity
from app.domain.entities.base_user import BaseUserEntity
class RegisterEntity(BaseUserEntity, BaseProfileEntity):
pass

View File

@ -0,0 +1,15 @@
from typing import Optional
from pydantic import BaseModel
from app.domain.entities.profile import ProfileEntity
class TeamEntity(BaseModel):
id: Optional[int] = None
title: str
description: Optional[str] = None
logo: Optional[str] = None
git_url: Optional[str] = None
profiles: Optional[list[ProfileEntity]] = None

View File

@ -0,0 +1,11 @@
from typing import Optional
from pydantic import BaseModel
class TokenEntity(BaseModel):
access_token: str
user_id: int
class Config:
from_attributes = True

View File

@ -0,0 +1,13 @@
from typing import Optional
from app.domain.entities.base_user import BaseUserEntity
from app.domain.entities.profile import ProfileEntity
class UserEntity(BaseUserEntity):
id: Optional[int] = None
password: Optional[str] = None
profile_id: int
profile: Optional[ProfileEntity] = None

View File

@ -1,3 +1,16 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Base = declarative_base()
from app.domain.models.contest_carousel_photos import ContestCarouselPhoto
from app.domain.models.contest_files import ContestFile
from app.domain.models.contests import Contest
from app.domain.models.contest_statuses import ContestStatus
from app.domain.models.projects import Project
from app.domain.models.project_files import ProjectFile
from app.domain.models.users import User
from app.domain.models.profiles import Profile
from app.domain.models.roles import Role
from app.domain.models.teams import Team
from app.domain.models.project_members import ProjectMember
from app.domain.models.profile_photos import ProfilePhoto

View File

@ -9,4 +9,4 @@ class ContestStatus(AdvancedBaseModel):
title = Column(VARCHAR(150), unique=True, nullable=False)
contest = relationship("Contest", back_populates="status")
contests = relationship("Contest", back_populates="status")

View File

@ -15,7 +15,7 @@ class Contest(AdvancedBaseModel):
is_win = Column(Boolean)
project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
status_id = Column(Integer, ForeignKey('contest_status.id'), nullable=False)
status_id = Column(Integer, ForeignKey('contest_statuses.id'), nullable=False)
project = relationship('Project', back_populates='contests')
status = relationship('ContestStatus', back_populates='contests')

View File

@ -0,0 +1,15 @@
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.domain.models.base import AdvancedBaseModel
class ProfilePhoto(AdvancedBaseModel):
__tablename__ = 'profile_photos'
filename = Column(String, nullable=False)
file_path = Column(String, nullable=False)
profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False)
profile = relationship('Profile', back_populates='profile_photos')

View File

@ -21,3 +21,5 @@ class Profile(AdvancedBaseModel):
team = relationship('Team', back_populates='profiles')
user = relationship('User', back_populates='profile')
profile_photos = relationship('ProfilePhoto', back_populates='profile')
projects = relationship('ProjectMember', back_populates='profile')

View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, ForeignKey, String
from sqlalchemy.orm import relationship
from app.domain.models.base import AdvancedBaseModel
class ProjectMember(AdvancedBaseModel):
__tablename__ = 'project_members'
description = Column(String)
project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False)
project = relationship('Project', back_populates='members')
profile = relationship('Profile', back_populates='projects')

View File

@ -10,5 +10,6 @@ class Project(AdvancedBaseModel):
description = Column(VARCHAR(150))
repository_url = Column(String, nullable=False)
contest = relationship("Contest", back_populates="project")
contests = relationship("Contest", back_populates="project")
files = relationship("ProjectFile", back_populates="project")
members = relationship("ProjectMember", back_populates="project")

View File

@ -12,4 +12,4 @@ class Team(AdvancedBaseModel):
logo = Column(String)
git_url = Column(String)
profile = relationship("Profile", back_populates="team")
profiles = relationship("Profile", back_populates="team")

View File

@ -1,5 +1,6 @@
from sqlalchemy import Column, VARCHAR, ForeignKey, Integer
from sqlalchemy import Column, VARCHAR, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from werkzeug.security import check_password_hash, generate_password_hash
from app.domain.models.base import AdvancedBaseModel
@ -8,8 +9,14 @@ class User(AdvancedBaseModel):
__tablename__ = 'users'
login = Column(VARCHAR(150), unique=True, nullable=False)
password = Column(VARCHAR(150), nullable=False)
password = Column(String, nullable=False)
profile_id = Column(Integer, ForeignKey('profiles.id'), nullable=False)
profile = relationship('Profile', back_populates='user')
def check_password(self, password):
return check_password_hash(self.password, password)
def set_password(self, password):
self.password = generate_password_hash(password)

View File

@ -0,0 +1,36 @@
import datetime
from typing import Optional
import jwt
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from app.application.users_repository import UsersRepository
from app.settings import get_auth_data
class AuthService:
def __init__(self, db: AsyncSession):
self.users_repository = UsersRepository(db)
async def authenticate_user(self, login: str, password: str) -> Optional[dict]:
user = await self.users_repository.get_by_login(login)
if user and user.check_password(password):
access_token = self.create_access_token({"user_id": user.id})
return {
"access_token": access_token,
"user_id": user.id
}
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid login or password")
@staticmethod
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
to_encode.update({"exp": expire})
auth_data = get_auth_data()
encode_jwt = jwt.encode(to_encode, auth_data['secret_key'], algorithm=auth_data['algorithm'])
return encode_jwt

View File

@ -0,0 +1,45 @@
from typing import Optional
import jwt
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
from app.application.users_repository import UsersRepository
from app.database.session import get_db
from app.domain.models.users import User
from app.settings import get_auth_data
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
db: AsyncSession = Depends(get_db)
) -> Optional[User]:
auth_data = get_auth_data()
try:
payload = jwt.decode(credentials.credentials, auth_data["secret_key"], algorithms=[auth_data["algorithm"]])
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
user_id = payload.get("user_id")
if user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
user = await UsersRepository(db).get_by_id_with_role(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
return user
def require_admin(user: User = Depends(get_current_user)) -> Optional[User]:
if user.profile.role.title != "Администратор":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
return user

View File

@ -0,0 +1,126 @@
import os
import uuid
import aiofiles
import magic
from fastapi import HTTPException, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from werkzeug.utils import secure_filename
from app.application.profile_photos_repository import ProfilePhotosRepository
from app.application.users_repository import UsersRepository
from app.domain.entities.profile_photo import ProfilePhotoEntity
from app.domain.models import ProfilePhoto, User
class ProfilePhotosService:
def __init__(self, db: AsyncSession):
self.profile_photos_repository = ProfilePhotosRepository(db)
self.users_repository = UsersRepository(db)
async def get_photo_file_by_id(self, photo_id: int) -> FileResponse:
photo = await self.profile_photos_repository.get_by_id(photo_id)
if not photo:
raise HTTPException(404, "Photo not found")
if not os.path.exists(photo.file_path):
raise HTTPException(404, "File not found on disk")
return FileResponse(
photo.file_path,
media_type=self.get_media_type(photo.filename),
filename=photo.filename,
)
async def get_photo_file_by_profile_id(self, profile_id: int) -> list[ProfilePhotoEntity]:
photos = await self.profile_photos_repository.get_by_profile_id(profile_id)
return [
self.model_to_entity(photo)
for photo in photos
]
async def upload_photo(self, profile_id: int, file: UploadFile, user: User):
user = await self.users_repository.get_by_id(user.id)
if profile_id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
self.validate_file_type(file)
photo = ProfilePhoto(
filename=self.generate_filename(file),
file_path=await self.save_file(file),
profile_id=profile_id
)
return self.model_to_entity(
await self.profile_photos_repository.create(photo)
)
async def delete_photo(self, photo_id: int, user: User) -> ProfilePhotoEntity:
user = await self.users_repository.get_by_id(user.id)
if user is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this ID was not found",
)
photo = await self.profile_photos_repository.get_by_id(photo_id)
if not photo:
raise HTTPException(404, "Photo not found")
if photo.profile_id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
if os.path.exists(photo.file_path):
os.remove(photo.file_path)
return self.model_to_entity(
await self.profile_photos_repository.delete(photo)
)
async def save_file(self, file: UploadFile, upload_dir: str = "uploads/profile_photos") -> str:
os.makedirs(upload_dir, exist_ok=True)
filename = self.generate_filename(file)
file_path = os.path.join(upload_dir, filename)
async with aiofiles.open(file_path, 'wb') as out_file:
content = await file.read()
await out_file.write(content)
return file_path
@staticmethod
def validate_file_type(file: UploadFile):
mime = magic.Magic(mime=True)
file_type = mime.from_buffer(file.file.read(1024))
file.file.seek(0)
if file_type not in ["image/jpeg", "image/png"]:
raise HTTPException(400, "Invalid file type")
@staticmethod
def generate_filename(file: UploadFile):
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
@staticmethod
def model_to_entity(profile_photo_model: ProfilePhoto) -> ProfilePhotoEntity:
return ProfilePhotoEntity(
id=profile_photo_model.id,
filename=profile_photo_model.filename,
file_path=profile_photo_model.file_path,
profile_id=profile_photo_model.profile_id,
)
@staticmethod
def get_media_type(filename: str) -> str:
extension = filename.split('.')[-1].lower()
return f"image/{extension}" if extension in ['jpeg', 'jpg', 'png'] else "application/octet-stream"

View File

@ -0,0 +1,135 @@
from typing import Optional
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.profiles_repository import ProfilesRepository
from app.application.roles_repository import RolesRepository
from app.application.teams_repository import TeamsRepository
from app.application.users_repository import UsersRepository
from app.domain.entities.profile import ProfileEntity
from app.domain.models import Profile, User
class ProfilesService:
def __init__(self, db: AsyncSession):
self.profiles_repository = ProfilesRepository(db)
self.teams_repository = TeamsRepository(db)
self.roles_repository = RolesRepository(db)
self.users_repository = UsersRepository(db)
async def create_profile(self, profile: ProfileEntity) -> Optional[ProfileEntity]:
team = await self.teams_repository.get_by_id(profile.team_id)
if team is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The team with this ID was not found',
)
role = await self.roles_repository.get_by_id(profile.role_id)
if role is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The role with this ID was not found',
)
profile_model = self.entity_to_model(profile)
profile_model = await self.profiles_repository.create(profile_model)
return self.model_to_entity(profile_model)
async def update_profile(self, profile_id: int, profile: ProfileEntity, user: User) -> Optional[
ProfileEntity
]:
user = await self.users_repository.get_by_id(user.id)
if user is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The user with this ID was not found',
)
profile_model = await self.profiles_repository.get_by_id(profile_id)
if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Permission denied',
)
team = await self.teams_repository.get_by_id(profile.team_id)
if team is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The team with this ID was not found',
)
role = await self.roles_repository.get_by_id(profile.role_id)
if role is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The role with this ID was not found',
)
profile_model.first_name = profile.first_name
profile_model.last_name = profile.last_name
profile_model.patronymic = profile.patronymic
profile_model.birthday = profile.birthday
profile_model.email = profile.email
profile_model.phone = profile.phone
profile_model.role_id = profile.role_id
profile_model.team_id = profile.team_id
profile_model = await self.profiles_repository.update(profile_model)
return self.model_to_entity(profile_model)
async def delete(self, profile_id: int, user: User):
user = await self.users_repository.get_by_id(user.id)
if user is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='The user with this ID was not found',
)
profile_model = await self.profiles_repository.get_by_id(profile_id)
if profile_model.id != user.profile_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Permission denied',
)
result = await self.profiles_repository.delete(profile_model)
return self.model_to_entity(result)
@staticmethod
def model_to_entity(profile_model: Profile) -> ProfileEntity:
return ProfileEntity(
id=profile_model.id,
first_name=profile_model.first_name,
last_name=profile_model.last_name,
patronymic=profile_model.patronymic,
birthday=profile_model.birthday,
email=profile_model.email,
phone=profile_model.phone,
role_id=profile_model.role_id,
team_id=profile_model.team_id,
)
@staticmethod
def entity_to_model(profile_entity: ProfileEntity) -> Profile:
profile_model = Profile(
first_name=profile_entity.first_name,
last_name=profile_entity.last_name,
patronymic=profile_entity.patronymic,
birthday=profile_entity.birthday,
email=profile_entity.email,
phone=profile_entity.phone,
role_id=profile_entity.role_id,
team_id=profile_entity.team_id,
)
if profile_entity.id is not None:
profile_model.id = profile_entity.id
return profile_model

View File

@ -0,0 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession
class ProjectsRepository:
def __init__(self, db: AsyncSession):
self.projects_repository = ProjectsRepository(db)

View File

@ -0,0 +1,75 @@
from typing import Optional
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.teams_repository import TeamsRepository
from app.domain.entities.team import TeamEntity
from app.domain.models import Team
class TeamsService:
def __init__(self, db: AsyncSession):
self.teams_repository = TeamsRepository(db)
async def get_all_teams(self) -> list[TeamEntity]:
teams = await self.teams_repository.get_all()
return [
self.model_to_entity(team)
for team in teams
]
async def create_team(self, team: TeamEntity) -> Optional[TeamEntity]:
team_model = self.entity_to_model(team)
await self.teams_repository.create(team_model)
return self.model_to_entity(team_model)
async def update_team(self, team_id: int, team: TeamEntity) -> Optional[TeamEntity]:
team_model = await self.teams_repository.get_by_id(team_id)
if not team_model:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found")
team_model.title = team.title
team_model.description = team_model.description
team_model.git_url = team_model.git_url
await self.teams_repository.update(team_model)
return self.model_to_entity(team_model)
async def delete_team(self, team_id: int) -> Optional[TeamEntity]:
team_model = await self.teams_repository.get_by_id(team_id)
if not team_model:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found")
result = await self.teams_repository.delete(team_model)
return self.model_to_entity(result)
@staticmethod
def model_to_entity(team_model: Team) -> TeamEntity:
return TeamEntity(
id=team_model.id,
title=team_model.title,
description=team_model.description,
logo=team_model.logo,
git_url=team_model.git_url,
)
@staticmethod
def entity_to_model(team_entity: TeamEntity) -> Team:
team_model = Team(
title=team_entity.title,
description=team_entity.description,
logo=team_entity.logo,
git_url=team_entity.git_url,
)
if team_entity.id:
team_model.id = team_entity.id
return team_model

View File

@ -0,0 +1,145 @@
from typing import Optional
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.profiles_repository import ProfilesRepository
from app.application.roles_repository import RolesRepository
from app.application.teams_repository import TeamsRepository
from app.application.users_repository import UsersRepository
from app.domain.entities.profile import ProfileEntity
from app.domain.entities.register import RegisterEntity
from app.domain.entities.user import UserEntity
from app.domain.models import User, Profile
class UsersService:
def __init__(self, db: AsyncSession):
self.users_repository = UsersRepository(db)
self.teams_repository = TeamsRepository(db)
self.roles_repository = RolesRepository(db)
self.profiles_repository = ProfilesRepository(db)
async def register_user(self, register_entity: RegisterEntity) -> Optional[UserEntity]:
team = await self.teams_repository.get_by_id(register_entity.team_id)
if team is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The team with this ID was not found",
)
role = await self.roles_repository.get_by_id(register_entity.role_id)
if role is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The role with this ID was not found",
)
if not self.is_strong_password(register_entity.password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The password is too weak",
)
user_model, profile_model = self.register_entity_to_models(register_entity)
profile_model = await self.profiles_repository.create(profile_model)
user_model.profile_id = profile_model.id
user_model = await self.users_repository.create(user_model)
user_entity = self.user_model_to_entity(user_model)
user_entity.profile = self.profile_model_to_entity(profile_model)
return user_entity
async def change_user_password(self, user_id: int, new_password: str, user: User) -> Optional[UserEntity]:
user_model = await self.users_repository.get_by_id(user_id)
if user_model is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The user with this ID was not found",
)
if user_model.id != user_id and user.profile.role.title != 'Администратор':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Permission denied",
)
if not self.is_strong_password(new_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The password is too weak",
)
user_model.set_password(new_password)
user_model = await self.users_repository.update(user_model)
return self.user_model_to_entity(user_model)
@staticmethod
def is_strong_password(password):
if len(password) < 8:
return False
if not any(char.isupper() for char in password):
return False
if not any(char.islower() for char in password):
return False
if not any(char.isdigit() for char in password):
return False
if not any(char in "!@#$%^&*()_+" for char in password):
return False
if not any(char.isalpha() for char in password):
return False
return True
@staticmethod
def register_entity_to_models(register_entity: RegisterEntity) -> tuple[User, Profile]:
user = User(
login=register_entity.login,
)
user.set_password(register_entity.password)
pofile = Profile(
first_name=register_entity.first_name,
last_name=register_entity.last_name,
patronymic=register_entity.patronymic,
birthday=register_entity.birthday,
email=register_entity.email,
phone=register_entity.phone,
role_id=register_entity.role_id,
team_id=register_entity.team_id
)
return user, pofile
@staticmethod
def profile_model_to_entity(profile_model: Profile) -> ProfileEntity:
return ProfileEntity(
id=profile_model.id,
first_name=profile_model.first_name,
last_name=profile_model.last_name,
patronymic=profile_model.patronymic,
birthday=profile_model.birthday,
email=profile_model.email,
phone=profile_model.phone,
role_id=profile_model.role_id,
team_id=profile_model.team_id,
)
@staticmethod
def user_model_to_entity(user_model: User) -> UserEntity:
return UserEntity(
id=user_model.id,
login=user_model.login,
profile_id=user_model.profile_id,
)

View File

@ -0,0 +1,39 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.contollers.auth_router import router as auth_router
from app.contollers.profile_photos_router import router as profile_photos_router
from app.contollers.profiles_router import router as profiles_router
from app.contollers.register_router import router as register_router
from app.contollers.teams_router import router as team_router
from app.contollers.users_router import router as users_router
from app.settings import settings
def start_app():
api_app = FastAPI()
api_app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
api_app.include_router(auth_router, prefix=f'{settings.PREFIX}/auth', tags=['auth'])
api_app.include_router(profile_photos_router, prefix=f'{settings.PREFIX}/profile_photos', tags=['profile_photos_router'])
api_app.include_router(profiles_router, prefix=f'{settings.PREFIX}/profiles', tags=['profiles'])
api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register'])
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
return api_app
app = start_app()
@app.get("/")
async def root():
return {"message": "Hello API"}

View File

@ -3,11 +3,9 @@ from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
LOG_LEVEL: str
LOG_FILE: str
SECRET_KEY: str
ALGORITHM: str
APP_PREFIX: str
PREFIX: str = ''
ALGORITHM: str = 'HS256'
class Config:
env_file = '.env'
@ -17,7 +15,7 @@ class Settings(BaseSettings):
settings = Settings()
def det_auth_data():
def get_auth_data():
return {
'secret_key': settings.SECRET_KEY,
'algorithm': settings.ALGORITHM

Binary file not shown.

25
WEB/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*/.vscode/*

3
WEB/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
WEB/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
WEB/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2043
WEB/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
WEB/package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@quasar/extras": "^1.16.17",
"axios": "^1.9.0",
"quasar": "^2.18.1",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
"sass-embedded": "^1.87.0",
"vite": "^6.3.1"
}
}

9
WEB/src/App.vue Normal file
View File

@ -0,0 +1,9 @@
<script setup>
</script>
<template>
<router-view />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,19 @@
import axios from "axios";
import CONFIG from "../../core/config.js";
const loginUser = async (loginData) => {
try {
const response = await axios.post(`${CONFIG.BASE_URL}/auth/`, loginData, {
withCredentials: true,
});
return response.data.access_token;
} catch (error) {
if (error.status === 401) {
throw new Error("Неверное имя пользователя или пароль")
}
throw new Error(error.message);
}
};
export default loginUser;

View File

@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

5
WEB/src/core/config.js Normal file
View File

@ -0,0 +1,5 @@
const CONFIG = {
BASE_URL: "http://127.0.0.1:8000",
};
export default CONFIG;

19
WEB/src/main.js Normal file
View File

@ -0,0 +1,19 @@
import { createApp } from 'vue'
import App from './App.vue'
import { Quasar, Notify, Dialog, Loading, QInput, QBtn, QForm, QCard, QCardSection } from 'quasar'
import router from './router'
import '@quasar/extras/material-icons/material-icons.css'
import 'quasar/src/css/index.sass'
const app = createApp(App)
app.use(Quasar, {
plugins: { Notify, Dialog, Loading }, // уведомления, диалоги, загрузка
components: { QInput, QBtn, QForm, QCard, QCardSection } // обязательно указать используемые компоненты
})
app.use(router)
app.mount('#app')

View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
12345
</template>
<style scoped>
</style>

View File

@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Notify } from 'quasar'
import loginUser from "../api/auth/loginRequest.js";
const router = useRouter()
const login = ref('')
const password = ref('')
const loading = ref(false)
const authorisation = async () => {
loading.value = true
try {
const accessToken = await loginUser({
login: login.value,
password: password.value
})
localStorage.setItem('access_token', accessToken)
Notify.create({
type: 'positive',
message: 'Успешный вход!'
})
router.push('/dashboard')
} catch (error) {
Notify.create({
type: 'negative',
message: error.message || 'Ошибка входа'
})
} finally {
loading.value = false
}
}
</script>
<template>
<div class="fullscreen flex flex-center bg-grey-2">
<q-card class="q-pa-lg shadow-2" style="width: 350px">
<q-card-section>
<div class="text-h6 text-center">Вход</div>
</q-card-section>
<q-card-section>
<q-form @submit.prevent="authorisation">
<q-input
v-model="login"
type="text"
label="Логин"
dense
outlined
autofocus
class="q-mb-md"
lazy-rules="ondemand"
:rules="[ val => (val && val.length > 0) || 'Введите логин' ]"
/>
<q-input
v-model="password"
type="password"
label="Пароль"
dense
outlined
class="q-mb-lg"
lazy-rules="ondemand"
:rules="[ val => (val && val.length > 0) || 'Введите пароль' ]"
/>
<q-btn
label="Войти"
type="submit"
color="primary"
class="full-width"
:loading="loading"
/>
</q-form>
</q-card-section>
</q-card>
</div>
</template>
<style scoped>
.fullscreen {
height: 100vh;
}
</style>

38
WEB/src/router/index.js Normal file
View File

@ -0,0 +1,38 @@
import {createRouter, createWebHistory} from 'vue-router'
// import DashboardPage from '@/pages/DashboardPage.vue'
import LoginPage from "../pages/LoginPage.vue";
import HomePage from "../pages/HomePage.vue"; // пример
const routes = [
{
path: '/', component: HomePage,
meta: {requiresAuth: true}
},
{path: '/login', component: LoginPage},
// {
// path: '/dashboard',
// component: DashboardPage,
// meta: { requiresAuth: true } // требовать авторизацию
// }
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Навешиваем перехватчик навигации
router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('access_token')
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else if (to.path === '/login' && isAuthenticated) {
next('/dashboard')
} else {
next()
}
})
export default router

79
WEB/src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

7
WEB/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})