added admin panel
This commit is contained in:
@@ -138,4 +138,68 @@ class CollectionUseCases:
|
||||
|
||||
all_collections = {c.collection_id: c for c in owned + public + accessed_collections}
|
||||
return list(all_collections.values())[skip:skip+limit]
|
||||
|
||||
async def list_collection_access(self, collection_id: UUID, user_id: UUID) -> list[CollectionAccess]:
|
||||
"""Получить список доступа к коллекции"""
|
||||
collection = await self.get_collection(collection_id)
|
||||
|
||||
has_access = await self.check_access(collection_id, user_id)
|
||||
if not has_access:
|
||||
raise ForbiddenError("У вас нет доступа к этой коллекции")
|
||||
|
||||
return await self.access_repository.list_by_collection(collection_id)
|
||||
|
||||
async def grant_access_by_telegram_id(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
telegram_id: str,
|
||||
owner_id: UUID
|
||||
) -> CollectionAccess:
|
||||
"""Предоставить доступ пользователю к коллекции по Telegram ID"""
|
||||
collection = await self.get_collection(collection_id)
|
||||
|
||||
if collection.owner_id != owner_id:
|
||||
raise ForbiddenError("Только владелец может предоставлять доступ")
|
||||
|
||||
user = await self.user_repository.get_by_telegram_id(telegram_id)
|
||||
if not user:
|
||||
from src.domain.entities.user import User, UserRole
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Creating new user with telegram_id: {telegram_id}")
|
||||
user = User(telegram_id=telegram_id, role=UserRole.USER)
|
||||
try:
|
||||
user = await self.user_repository.create(user)
|
||||
logger.info(f"User created successfully: user_id={user.user_id}, telegram_id={user.telegram_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating user: {e}")
|
||||
raise
|
||||
|
||||
if user.user_id == owner_id:
|
||||
raise ForbiddenError("Владелец уже имеет доступ к коллекции")
|
||||
|
||||
existing_access = await self.access_repository.get_by_user_and_collection(user.user_id, collection_id)
|
||||
if existing_access:
|
||||
return existing_access
|
||||
|
||||
access = CollectionAccess(user_id=user.user_id, collection_id=collection_id)
|
||||
return await self.access_repository.create(access)
|
||||
|
||||
async def revoke_access_by_telegram_id(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
telegram_id: str,
|
||||
owner_id: UUID
|
||||
) -> bool:
|
||||
"""Отозвать доступ пользователя к коллекции по Telegram ID"""
|
||||
collection = await self.get_collection(collection_id)
|
||||
|
||||
if collection.owner_id != owner_id:
|
||||
raise ForbiddenError("Только владелец может отзывать доступ")
|
||||
|
||||
user = await self.user_repository.get_by_telegram_id(telegram_id)
|
||||
if not user:
|
||||
raise NotFoundError(f"Пользователь с telegram_id {telegram_id} не найден")
|
||||
|
||||
return await self.access_repository.delete_by_user_and_collection(user.user_id, collection_id)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import BinaryIO, Optional
|
||||
from src.domain.entities.document import Document
|
||||
from src.domain.repositories.document_repository import IDocumentRepository
|
||||
from src.domain.repositories.collection_repository import ICollectionRepository
|
||||
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
|
||||
from src.application.services.document_parser_service import DocumentParserService
|
||||
from src.shared.exceptions import NotFoundError, ForbiddenError
|
||||
|
||||
@@ -17,12 +18,25 @@ class DocumentUseCases:
|
||||
self,
|
||||
document_repository: IDocumentRepository,
|
||||
collection_repository: ICollectionRepository,
|
||||
access_repository: ICollectionAccessRepository,
|
||||
parser_service: DocumentParserService
|
||||
):
|
||||
self.document_repository = document_repository
|
||||
self.collection_repository = collection_repository
|
||||
self.access_repository = access_repository
|
||||
self.parser_service = parser_service
|
||||
|
||||
async def _check_collection_access(self, user_id: UUID, collection) -> bool:
|
||||
"""Проверить доступ пользователя к коллекции"""
|
||||
if collection.owner_id == user_id:
|
||||
return True
|
||||
|
||||
if collection.is_public:
|
||||
return True
|
||||
|
||||
access = await self.access_repository.get_by_user_and_collection(user_id, collection.collection_id)
|
||||
return access is not None
|
||||
|
||||
async def create_document(
|
||||
self,
|
||||
collection_id: UUID,
|
||||
@@ -55,8 +69,9 @@ class DocumentUseCases:
|
||||
if not collection:
|
||||
raise NotFoundError(f"Коллекция {collection_id} не найдена")
|
||||
|
||||
if collection.owner_id != user_id:
|
||||
raise ForbiddenError("Только владелец может добавлять документы")
|
||||
has_access = await self._check_collection_access(user_id, collection)
|
||||
if not has_access:
|
||||
raise ForbiddenError("У вас нет доступа к этой коллекции")
|
||||
|
||||
title, content = await self.parser_service.parse_pdf(file, filename)
|
||||
|
||||
@@ -87,8 +102,11 @@ class DocumentUseCases:
|
||||
document = await self.get_document(document_id)
|
||||
|
||||
collection = await self.collection_repository.get_by_id(document.collection_id)
|
||||
if not collection or collection.owner_id != user_id:
|
||||
raise ForbiddenError("Только владелец коллекции может изменять документы")
|
||||
if not collection:
|
||||
raise NotFoundError(f"Коллекция {document.collection_id} не найдена")
|
||||
has_access = await self._check_collection_access(user_id, collection)
|
||||
if not has_access:
|
||||
raise ForbiddenError("У вас нет доступа к этой коллекции")
|
||||
|
||||
if title is not None:
|
||||
document.title = title
|
||||
|
||||
@@ -13,7 +13,9 @@ from src.presentation.schemas.collection_schemas import (
|
||||
CollectionUpdate,
|
||||
CollectionResponse,
|
||||
CollectionAccessGrant,
|
||||
CollectionAccessResponse
|
||||
CollectionAccessResponse,
|
||||
CollectionAccessListResponse,
|
||||
CollectionAccessUserInfo
|
||||
)
|
||||
from src.application.use_cases.collection_use_cases import CollectionUseCases
|
||||
from src.domain.entities.user import User
|
||||
@@ -44,10 +46,19 @@ async def create_collection(
|
||||
@inject
|
||||
async def get_collection(
|
||||
collection_id: UUID,
|
||||
request: Request,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()],
|
||||
use_cases: Annotated[CollectionUseCases, FromDishka()]
|
||||
):
|
||||
"""Получить коллекцию по ID"""
|
||||
current_user = await get_current_user(request, user_repo)
|
||||
collection = await use_cases.get_collection(collection_id)
|
||||
|
||||
has_access = await use_cases.check_access(collection_id, current_user.user_id)
|
||||
if not has_access:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="У вас нет доступа к этой коллекции")
|
||||
|
||||
return CollectionResponse.from_entity(collection)
|
||||
|
||||
|
||||
@@ -138,3 +149,78 @@ async def revoke_access(
|
||||
await use_cases.revoke_access(collection_id, user_id, current_user.user_id)
|
||||
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
|
||||
|
||||
|
||||
@router.get("/{collection_id}/access", response_model=List[CollectionAccessListResponse])
|
||||
@inject
|
||||
async def list_collection_access(
|
||||
collection_id: UUID,
|
||||
request: Request,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()],
|
||||
use_cases: Annotated[CollectionUseCases, FromDishka()]
|
||||
):
|
||||
"""Получить список пользователей с доступом к коллекции"""
|
||||
current_user = await get_current_user(request, user_repo)
|
||||
accesses = await use_cases.list_collection_access(collection_id, current_user.user_id)
|
||||
result = []
|
||||
for access in accesses:
|
||||
user = await user_repo.get_by_id(access.user_id)
|
||||
if user:
|
||||
user_info = CollectionAccessUserInfo(
|
||||
user_id=user.user_id,
|
||||
telegram_id=user.telegram_id,
|
||||
role=user.role.value,
|
||||
created_at=user.created_at
|
||||
)
|
||||
result.append(CollectionAccessListResponse(
|
||||
access_id=access.access_id,
|
||||
user=user_info,
|
||||
collection_id=access.collection_id,
|
||||
created_at=access.created_at
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{collection_id}/access/telegram/{telegram_id}", response_model=CollectionAccessResponse, status_code=status.HTTP_201_CREATED)
|
||||
@inject
|
||||
async def grant_access_by_telegram_id(
|
||||
collection_id: UUID,
|
||||
telegram_id: str,
|
||||
request: Request,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()],
|
||||
use_cases: Annotated[CollectionUseCases, FromDishka()]
|
||||
):
|
||||
"""Предоставить доступ пользователю к коллекции по Telegram ID"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
current_user = await get_current_user(request, user_repo)
|
||||
logger.info(f"Granting access: collection_id={collection_id}, target_telegram_id={telegram_id}, owner_id={current_user.user_id}")
|
||||
|
||||
try:
|
||||
access = await use_cases.grant_access_by_telegram_id(
|
||||
collection_id=collection_id,
|
||||
telegram_id=telegram_id,
|
||||
owner_id=current_user.user_id
|
||||
)
|
||||
logger.info(f"Access granted successfully: access_id={access.access_id}")
|
||||
return CollectionAccessResponse.from_entity(access)
|
||||
except Exception as e:
|
||||
logger.error(f"Error granting access: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@router.delete("/{collection_id}/access/telegram/{telegram_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@inject
|
||||
async def revoke_access_by_telegram_id(
|
||||
collection_id: UUID,
|
||||
telegram_id: str,
|
||||
request: Request,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()],
|
||||
use_cases: Annotated[CollectionUseCases, FromDishka()]
|
||||
):
|
||||
"""Отозвать доступ пользователя к коллекции по Telegram ID"""
|
||||
current_user = await get_current_user(request, user_repo)
|
||||
await use_cases.revoke_access_by_telegram_id(collection_id, telegram_id, current_user.user_id)
|
||||
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
API роутеры для работы с документами
|
||||
"""
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, status, UploadFile, File, Depends, Request
|
||||
from fastapi import APIRouter, status, UploadFile, File, Depends, Request, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List, Annotated
|
||||
from dishka.integrations.fastapi import FromDishka, inject
|
||||
@@ -14,6 +14,7 @@ from src.presentation.schemas.document_schemas import (
|
||||
DocumentResponse
|
||||
)
|
||||
from src.application.use_cases.document_use_cases import DocumentUseCases
|
||||
from src.application.use_cases.collection_use_cases import CollectionUseCases
|
||||
from src.domain.entities.user import User
|
||||
|
||||
router = APIRouter(prefix="/documents", tags=["documents"])
|
||||
@@ -41,10 +42,10 @@ async def create_document(
|
||||
@router.post("/upload", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
|
||||
@inject
|
||||
async def upload_document(
|
||||
collection_id: UUID,
|
||||
request: Request,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()],
|
||||
use_cases: Annotated[DocumentUseCases, FromDishka()],
|
||||
collection_id: UUID = Query(...),
|
||||
request: Request = None,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()] = None,
|
||||
use_cases: Annotated[DocumentUseCases, FromDishka()] = None,
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
"""Загрузить и распарсить PDF документ или изображение"""
|
||||
@@ -123,11 +124,22 @@ async def delete_document(
|
||||
@inject
|
||||
async def list_collection_documents(
|
||||
collection_id: UUID,
|
||||
request: Request,
|
||||
user_repo: Annotated[IUserRepository, FromDishka()],
|
||||
use_cases: Annotated[DocumentUseCases, FromDishka()],
|
||||
collection_use_cases: Annotated[CollectionUseCases, FromDishka()],
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
):
|
||||
"""Получить документы коллекции"""
|
||||
current_user = await get_current_user(request, user_repo)
|
||||
|
||||
|
||||
has_access = await collection_use_cases.check_access(collection_id, current_user.user_id)
|
||||
if not has_access:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403, detail="У вас нет доступа к этой коллекции")
|
||||
|
||||
documents = await use_cases.list_collection_documents(
|
||||
collection_id=collection_id,
|
||||
skip=skip,
|
||||
|
||||
@@ -75,3 +75,22 @@ class CollectionAccessResponse(BaseModel):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CollectionAccessUserInfo(BaseModel):
|
||||
"""Информация о пользователе с доступом"""
|
||||
user_id: UUID
|
||||
telegram_id: str
|
||||
role: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class CollectionAccessListResponse(BaseModel):
|
||||
"""Схема ответа со списком доступа"""
|
||||
access_id: UUID
|
||||
user: CollectionAccessUserInfo
|
||||
collection_id: UUID
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@@ -151,9 +151,10 @@ class UseCaseProvider(Provider):
|
||||
self,
|
||||
document_repo: IDocumentRepository,
|
||||
collection_repo: ICollectionRepository,
|
||||
access_repo: ICollectionAccessRepository,
|
||||
parser_service: DocumentParserService
|
||||
) -> DocumentUseCases:
|
||||
return DocumentUseCases(document_repo, collection_repo, parser_service)
|
||||
return DocumentUseCases(document_repo, collection_repo, access_repo, parser_service)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_conversation_use_cases(
|
||||
|
||||
Reference in New Issue
Block a user