added admin panel

This commit is contained in:
2025-12-24 06:28:01 +03:00
parent 169d874dad
commit c4b3521257
10 changed files with 1199 additions and 27 deletions
@@ -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
+87 -1
View File
@@ -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)
+17 -5
View File
@@ -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
+2 -1
View File
@@ -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(