Compare commits
10 Commits
9b5b9516cc
...
0aee1e625b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0aee1e625b | |||
| 2903330a2f | |||
|
|
e58752fa99 | ||
|
|
0e10f43dea | ||
| 94e1c755a0 | |||
| ac64bbe24c | |||
| bdb428a1cb | |||
| 9187518c8b | |||
|
|
3d26a7f212 | ||
|
|
c0f0293ce7 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ API/.idea
|
||||
|
||||
API/.venv/
|
||||
API/.idea/
|
||||
API/.env
|
||||
|
||||
32
API/app/application/profile_photos_repository.py
Normal file
32
API/app/application/profile_photos_repository.py
Normal 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
|
||||
32
API/app/application/profiles_repository.py
Normal file
32
API/app/application/profiles_repository.py
Normal 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
|
||||
37
API/app/application/projects_repository.py
Normal file
37
API/app/application/projects_repository.py
Normal 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
|
||||
16
API/app/application/roles_repository.py
Normal file
16
API/app/application/roles_repository.py
Normal 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()
|
||||
37
API/app/application/teams_repository.py
Normal file
37
API/app/application/teams_repository.py
Normal 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
|
||||
47
API/app/application/users_repository.py
Normal file
47
API/app/application/users_repository.py
Normal 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
|
||||
33
API/app/contollers/auth_router.py
Normal file
33
API/app/contollers/auth_router.py
Normal 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
|
||||
69
API/app/contollers/profile_photos_router.py
Normal file
69
API/app/contollers/profile_photos_router.py
Normal 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)
|
||||
57
API/app/contollers/profiles_router.py
Normal file
57
API/app/contollers/profiles_router.py
Normal 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)
|
||||
26
API/app/contollers/register_router.py
Normal file
26
API/app/contollers/register_router.py
Normal 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
|
||||
71
API/app/contollers/teams_router.py
Normal file
71
API/app/contollers/teams_router.py
Normal 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)
|
||||
27
API/app/contollers/users_router.py
Normal file
27
API/app/contollers/users_router.py
Normal 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)
|
||||
@ -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 ###
|
||||
@ -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 ###
|
||||
15
API/app/database/session.py
Normal file
15
API/app/database/session.py
Normal 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
|
||||
5
API/app/domain/entities/auth.py
Normal file
5
API/app/domain/entities/auth.py
Normal file
@ -0,0 +1,5 @@
|
||||
from app.domain.entities.base_user import BaseUserEntity
|
||||
|
||||
|
||||
class AuthEntity(BaseUserEntity):
|
||||
pass
|
||||
19
API/app/domain/entities/base_profile.py
Normal file
19
API/app/domain/entities/base_profile.py
Normal 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
|
||||
9
API/app/domain/entities/base_user.py
Normal file
9
API/app/domain/entities/base_user.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BaseUserEntity(BaseModel):
|
||||
login: str
|
||||
password: str
|
||||
|
||||
class Config:
|
||||
abstract = True
|
||||
7
API/app/domain/entities/profile.py
Normal file
7
API/app/domain/entities/profile.py
Normal file
@ -0,0 +1,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from app.domain.entities.base_profile import BaseProfileEntity
|
||||
|
||||
|
||||
class ProfileEntity(BaseProfileEntity):
|
||||
id: Optional[int] = None
|
||||
8
API/app/domain/entities/profile_photo.py
Normal file
8
API/app/domain/entities/profile_photo.py
Normal file
@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ProfilePhotoEntity(BaseModel):
|
||||
id: int
|
||||
filename: str
|
||||
file_path: str
|
||||
profile_id: int
|
||||
9
API/app/domain/entities/project.py
Normal file
9
API/app/domain/entities/project.py
Normal 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
|
||||
6
API/app/domain/entities/register.py
Normal file
6
API/app/domain/entities/register.py
Normal 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
|
||||
15
API/app/domain/entities/team.py
Normal file
15
API/app/domain/entities/team.py
Normal 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
|
||||
11
API/app/domain/entities/token_entity.py
Normal file
11
API/app/domain/entities/token_entity.py
Normal 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
|
||||
13
API/app/domain/entities/user.py
Normal file
13
API/app/domain/entities/user.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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')
|
||||
|
||||
15
API/app/domain/models/profile_photos.py
Normal file
15
API/app/domain/models/profile_photos.py
Normal 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')
|
||||
@ -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')
|
||||
|
||||
16
API/app/domain/models/project_members.py
Normal file
16
API/app/domain/models/project_members.py
Normal 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')
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
36
API/app/infrastructure/auth_service.py
Normal file
36
API/app/infrastructure/auth_service.py
Normal 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
|
||||
45
API/app/infrastructure/dependencies.py
Normal file
45
API/app/infrastructure/dependencies.py
Normal 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
|
||||
126
API/app/infrastructure/profile_photos_service.py
Normal file
126
API/app/infrastructure/profile_photos_service.py
Normal 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"
|
||||
135
API/app/infrastructure/profiles_service.py
Normal file
135
API/app/infrastructure/profiles_service.py
Normal 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
|
||||
6
API/app/infrastructure/projects_service.py
Normal file
6
API/app/infrastructure/projects_service.py
Normal file
@ -0,0 +1,6 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class ProjectsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.projects_repository = ProjectsRepository(db)
|
||||
75
API/app/infrastructure/teams_service.py
Normal file
75
API/app/infrastructure/teams_service.py
Normal 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
|
||||
145
API/app/infrastructure/users_service.py
Normal file
145
API/app/infrastructure/users_service.py
Normal 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,
|
||||
)
|
||||
@ -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"}
|
||||
@ -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
|
||||
|
||||
BIN
API/req.txt
BIN
API/req.txt
Binary file not shown.
25
WEB/.gitignore
vendored
Normal file
25
WEB/.gitignore
vendored
Normal 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
3
WEB/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
WEB/README.md
Normal file
5
WEB/README.md
Normal 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
13
WEB/index.html
Normal 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
2043
WEB/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
WEB/package.json
Normal file
23
WEB/package.json
Normal 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
9
WEB/src/App.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
19
WEB/src/api/auth/loginRequest.js
Normal file
19
WEB/src/api/auth/loginRequest.js
Normal 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;
|
||||
43
WEB/src/components/HelloWorld.vue
Normal file
43
WEB/src/components/HelloWorld.vue
Normal 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
5
WEB/src/core/config.js
Normal 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
19
WEB/src/main.js
Normal 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')
|
||||
11
WEB/src/pages/HomePage.vue
Normal file
11
WEB/src/pages/HomePage.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
12345
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
91
WEB/src/pages/LoginPage.vue
Normal file
91
WEB/src/pages/LoginPage.vue
Normal 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
38
WEB/src/router/index.js
Normal 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
79
WEB/src/style.css
Normal 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
7
WEB/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user