From 0caf1f3f42e889e9b1a18617231ab504403bd0c0 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 4 Jul 2025 15:12:21 +0400 Subject: [PATCH] feat: deals viewer mode and links for viewers --- constants.py | 4 ++ routers/billing.py | 3 +- routers/card.py | 20 +++++--- routers/chat.py | 4 +- routers/client.py | 12 +++++ routers/marketplace.py | 21 +++++--- routers/product.py | 3 +- routers/project.py | 3 +- schemas/auth.py | 16 +++++- schemas/client.py | 5 ++ services/auth.py | 33 ++++++++++--- services/billing.py | 8 ++- services/card.py | 107 +++++++++++++++++++++++------------------ services/card_group.py | 6 +-- services/chat.py | 6 ++- services/client.py | 6 +++ services/product.py | 7 ++- services/status.py | 19 ++++++-- utils/auth.py | 46 ++++++++++++++++++ 19 files changed, 246 insertions(+), 83 deletions(-) create mode 100644 utils/auth.py diff --git a/constants.py b/constants.py index 63ec3fa..1ad67e9 100644 --- a/constants.py +++ b/constants.py @@ -60,3 +60,7 @@ DEAL_STATUS_STR = [ # 30 minutes in seconds INVITE_CODE_EXPIRY = 30 * 60 + + +DEAL_EDITOR = 'deal_editor' +DEALS_VIEWER = 'deals_viewer' diff --git a/routers/billing.py b/routers/billing.py index 1e962e3..3c00304 100644 --- a/routers/billing.py +++ b/routers/billing.py @@ -45,8 +45,9 @@ async def create_deal_bill( async def cancel_deal_billing( session: SessionDependency, request: CancelCardBillRequest, + user: CurrentUserDependency, ): - return await BillingService(session).cancel_card_billing(request) + return await BillingService(session).cancel_card_billing(user, request) @billing_router.get( diff --git a/routers/card.py b/routers/card.py index b0b2d4a..d1b7c52 100644 --- a/routers/card.py +++ b/routers/card.py @@ -12,7 +12,7 @@ from models import User from parsers import DealParser from schemas.barcode import GetCardProductsBarcodesPdfRequest, GetCardProductsBarcodesPdfResponse from schemas.card import * -from services.auth import get_current_user, authorized_user, guest_user +from services.auth import get_current_user, authorized_user, guest_user, user_deals_viewer, user_deal_editor from services.barcode import BarcodeService from services.billing import BillingService from services.card import CardsService @@ -82,14 +82,14 @@ async def change_status( '/summaries', response_model=CardSummaryResponse, operation_id='getCardSummaries', - dependencies=[Depends(authorized_user)] ) async def get_summary( session: Annotated[AsyncSession, Depends(get_session)], request: GetCardSummariesRequest, pagination: PaginationDependency, + user: CurrentUserDependency, ): - return await CardsService(session).get_summary(request, pagination) + return await CardsService(session).get_summary(request, pagination, user) @card_router.post( @@ -151,13 +151,13 @@ async def update_general_info( '/update-products-and-services-general-info', response_model=ProductsAndServicesGeneralInfoResponse, operation_id='update_products_and_services_general_info', - dependencies=[Depends(authorized_user)] ) async def update_products_and_services_general_info( request: ProductsAndServicesGeneralInfoRequest, session: Annotated[AsyncSession, Depends(get_session)], + user: CurrentUserDependency, ): - return await CardsService(session).update_products_and_services_general_info(request) + return await CardsService(session).update_products_and_services_general_info(request, user) @card_router.post( @@ -185,6 +185,7 @@ async def update_card_client( ): return await CardsService(session).update_card_client(request) + @card_router.post( '/add-kit', response_model=CardAddKitResponse, @@ -206,9 +207,8 @@ async def add_kit_to_card( async def create_guest_url( session: SessionDependency, request: CardCreateGuestUrlRequest, - user: CurrentUserDependency ): - return CardsService(session).create_guest_url(user, request) + return await CardsService(session).create_guest_url(request) @card_router.get( @@ -267,6 +267,7 @@ async def recalculate_card_price( '/employee', response_model=ManageEmployeeResponse, operation_id='manage_employee', + dependencies=[Depends(user_deal_editor)], ) async def manage_employee( session: SessionDependency, @@ -279,6 +280,7 @@ async def manage_employee( '/employee/available/{card_id}', response_model=GetAvailableEmployeesToAssignResponse, operation_id='get_available_employees_to_assign', + dependencies=[Depends(user_deal_editor)], ) async def get_available_employees_to_assign( session: Annotated[AsyncSession, Depends(get_session)], @@ -291,6 +293,7 @@ async def get_available_employees_to_assign( '/prefill/excel/parse', response_model=ParseCardsExcelResponse, operation_id='parse_deals_excel', + dependencies=[Depends(authorized_user)] ) async def parse_deals_excel( session: Annotated[AsyncSession, Depends(get_session)], @@ -304,6 +307,7 @@ async def parse_deals_excel( '/prefill/excel/create', response_model=CreateCardsFromExcelResponse, operation_id='create_deals_excel', + dependencies=[Depends(authorized_user)], ) async def create_deals_from_excel( session: Annotated[AsyncSession, Depends(get_session)], @@ -312,6 +316,7 @@ async def create_deals_from_excel( ): return await CardsService(session).create_cards_from_excel(request, user) + # endregion # region Card services @@ -519,5 +524,4 @@ async def get_card_products_barcodes_pdf( mime_type='application/pdf' ) - # endregion diff --git a/routers/chat.py b/routers/chat.py index d402abd..d5800b0 100644 --- a/routers/chat.py +++ b/routers/chat.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, UploadFile from sqlalchemy.ext.asyncio import AsyncSession +from backend.dependecies import CurrentUserDependency from backend.session import get_session from models import User from schemas.chat import * @@ -111,8 +112,9 @@ async def get_client_chats_list( async def create_chat( session: Annotated[AsyncSession, Depends(get_session)], request: CreateChatRequest, + user: CurrentUserDependency, ): - return await ChatService(session).create_chat(request) + return await ChatService(session).create_chat(request, user) @chat_router.post( diff --git a/routers/client.py b/routers/client.py index cfa131d..e663f91 100644 --- a/routers/client.py +++ b/routers/client.py @@ -98,3 +98,15 @@ async def delete_client( session: Annotated[AsyncSession, Depends(get_session)] ): return await ClientService(session).delete(request) + + +@client_router.post( + '/link/{client_id}', + operation_id='client_link', + response_model=CreateGuestUrlResponse, +) +async def client_link( + session: Annotated[AsyncSession, Depends(get_session)], + client_id: int, +): + return await ClientService(session).create_guest_url(client_id) diff --git a/routers/marketplace.py b/routers/marketplace.py index 3b786ab..d58a839 100644 --- a/routers/marketplace.py +++ b/routers/marketplace.py @@ -8,17 +8,16 @@ from services.marketplace import MarketplaceService marketplace_router = APIRouter( prefix="/marketplace", tags=["marketplace"], - dependencies=[Depends(authorized_user)] ) @marketplace_router.get( '/base/get-all', operation_id='get_all_base_marketplaces', - response_model=GetAllBaseMarketplacesResponse + response_model=GetAllBaseMarketplacesResponse, ) async def get_all( - session: SessionDependency + session: SessionDependency, ): return await MarketplaceService(session).get_all_base_marketplaces() @@ -26,7 +25,8 @@ async def get_all( @marketplace_router.post( '/get', operation_id='get_client_marketplaces', - response_model=GetClientMarketplacesResponse + response_model=GetClientMarketplacesResponse, + dependencies=[Depends(authorized_user)] ) async def get( session: SessionDependency, @@ -38,7 +38,8 @@ async def get( @marketplace_router.post( '/create', operation_id='create_marketplace', - response_model=CreateMarketplaceResponse + response_model=CreateMarketplaceResponse, + dependencies=[Depends(authorized_user)], ) async def create( session: SessionDependency, @@ -50,7 +51,8 @@ async def create( @marketplace_router.post( '/delete', operation_id='delete_marketplace', - response_model=DeleteMarketplaceResponse + response_model=DeleteMarketplaceResponse, + dependencies=[Depends(authorized_user)], ) async def delete( session: SessionDependency, @@ -62,7 +64,8 @@ async def delete( @marketplace_router.post( '/update', operation_id='update_marketplace', - response_model=UpdateMarketplaceResponse + response_model=UpdateMarketplaceResponse, + dependencies=[Depends(authorized_user)], ) async def update( session: SessionDependency, @@ -70,10 +73,12 @@ async def update( ): return await MarketplaceService(session).update_marketplace(request) + @marketplace_router.post( '/yandex-market/get-campaigns', operation_id='get_yandex_market_campaigns', - response_model=GetYandexMarketCampaignsResponse + response_model=GetYandexMarketCampaignsResponse, + dependencies=[Depends(authorized_user)], ) async def get_yandex_market_campaigns( session: SessionDependency, diff --git a/routers/product.py b/routers/product.py index 1696332..0bee32e 100644 --- a/routers/product.py +++ b/routers/product.py @@ -31,8 +31,9 @@ product_router = APIRouter( async def create_product( request: ProductCreateRequest, session: Annotated[AsyncSession, Depends(get_session)], + user: CurrentUserDependency, ): - return await ProductService(session).create(request) + return await ProductService(session).create(request, user) @product_router.post( diff --git a/routers/project.py b/routers/project.py index 776575f..a1c60f2 100644 --- a/routers/project.py +++ b/routers/project.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.session import get_session from schemas.project import * -from services.auth import guest_user, authorized_user +from services.auth import guest_user, authorized_user, user_deals_viewer from services.project import ProjectService project_router = APIRouter( @@ -18,6 +18,7 @@ project_router = APIRouter( "/", response_model=GetProjectsResponse, operation_id="get_projects", + dependencies=[Depends(user_deals_viewer)], ) async def get_projects( session: Annotated[AsyncSession, Depends(get_session)], diff --git a/schemas/auth.py b/schemas/auth.py index 91e12ef..e37822f 100644 --- a/schemas/auth.py +++ b/schemas/auth.py @@ -1,5 +1,6 @@ -from typing import Optional +from typing import Optional, TypedDict, Union +from models import User from schemas.base import BaseSchema, CustomModelSnake @@ -14,3 +15,16 @@ class AuthLoginRequest(CustomModelSnake): class AuthLoginResponse(BaseSchema): access_token: str + + +class UserDealEditor(TypedDict): + deal_id: int + sub: str + + +class UserViewer(TypedDict): + client_id: int + sub: str + + +UserUnion = Union[User, UserViewer, UserDealEditor] diff --git a/schemas/client.py b/schemas/client.py index 4d3c019..2fbcdef 100644 --- a/schemas/client.py +++ b/schemas/client.py @@ -87,4 +87,9 @@ class ClientUpdateResponse(OkMessageSchema): class ClientDeleteResponse(OkMessageSchema): pass + + +class CreateGuestUrlResponse(OkMessageSchema): + url: str + # endregion diff --git a/services/auth.py b/services/auth.py index cd5e602..2b5f0d4 100644 --- a/services/auth.py +++ b/services/auth.py @@ -11,6 +11,7 @@ from starlette import status import backend.config import constants from backend.session import get_session +from constants import DEALS_VIEWER, DEAL_EDITOR from enums.user import UserRole from models import User, InviteCode from schemas.auth import * @@ -23,7 +24,7 @@ algorithm = 'HS256' async def get_current_user( session: Annotated[AsyncSession, Depends(get_session)], token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)] -) -> Union[User, None, dict]: +) -> Optional[UserUnion]: if not token.credentials: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') try: @@ -31,11 +32,11 @@ async def get_current_user( user_id = payload.get('sub') if not user_id: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid credentials') - if user_id == 'guest': + if user_id == DEAL_EDITOR or user_id == DEALS_VIEWER: return payload user_id = int(user_id) - user = await session.get(User, user_id) + user: Optional[User] = await session.get(User, user_id) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials') if user.is_deleted or user.is_blocked: @@ -53,12 +54,24 @@ async def authorized_user( raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') -async def guest_user(user: Annotated[User, Depends(get_current_user)]): +async def guest_user(user: Annotated[UserUnion, Depends(get_current_user)]): if (type(user) is User) or (type(user) is dict): return user raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') +async def user_deals_viewer(user: Annotated[UserUnion, Depends(get_current_user)]): + if (type(user) is User) or (type(user) is dict and user['sub'] == DEALS_VIEWER): + return user + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') + + +async def user_deal_editor(user: Annotated[UserUnion, Depends(get_current_user)]): + if (type(user) is User) or (type(user) is dict and user['sub'] == DEAL_EDITOR): + return user + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') + + class AuthService(BaseService): @staticmethod def _generate_jwt_token(payload: dict) -> str: @@ -101,9 +114,17 @@ class AuthService(BaseService): return AuthLoginResponse(access_token=access_token) def create_deal_guest_token(self, deal_id: int): - payload = { - 'sub': 'guest', + payload: UserDealEditor = { + 'sub': constants.DEAL_EDITOR, 'deal_id': deal_id } return self._generate_jwt_token(payload) + + def create_client_guest_token(self, client_id: int): + payload: UserViewer = { + 'sub': constants.DEALS_VIEWER, + 'client_id': client_id + } + + return self._generate_jwt_token(payload) diff --git a/services/billing.py b/services/billing.py index 1d9c2d1..dac0291 100644 --- a/services/billing.py +++ b/services/billing.py @@ -17,9 +17,11 @@ from external.billing import BillingClient, CreateBillingRequestValue, CreateBil ProductBillingDocumentPdf, ServiceBillingDocumentPdf from models import CardBillRequest, Card, CardProduct, CardService as CardServiceModel, CardGroup, \ GroupBillRequest +from schemas.auth import UserUnion from schemas.billing import * from services.base import BaseService from services.card import CardsService +from utils.auth import verify_user_deal_editor from utils.list_utils import to_locale_number @@ -157,6 +159,8 @@ class BillingService(BaseService): async def create_card_billing(self, user, request: CreateCardBillRequest) -> CreateCardBillResponse: try: + verify_user_deal_editor(user) + card_service = CardsService(self.session) billing_client = BillingClient(backend.config.BILLING_API_KEY) @@ -261,8 +265,10 @@ class BillingService(BaseService): await self.session.delete(bill) - async def cancel_card_billing(self, request: CancelCardBillRequest) -> CancelCardBillResponse: + async def cancel_card_billing(self, user: UserUnion, request: CancelCardBillRequest) -> CancelCardBillResponse: try: + verify_user_deal_editor(user) + card = await self._get_card_by_id(request.card_id) if not card: return CancelCardBillResponse(ok=False, message='Сделка не найдена') diff --git a/services/card.py b/services/card.py index 2d516b2..3df2d53 100644 --- a/services/card.py +++ b/services/card.py @@ -2,14 +2,15 @@ from collections import defaultdict import lexorank import math +import starlette.status from fastapi import HTTPException from sqlalchemy import select, func, update, delete, insert, and_, Select from sqlalchemy.orm import joinedload, selectinload, noload -from starlette import status from card_attributes import CardAttributesCommandHandler from card_attributes.exceptions import CardAttributeException from models import * +from schemas.auth import UserUnion from schemas.base import PaginationSchema from schemas.card import * from schemas.client import ClientDetailsSchema @@ -20,20 +21,13 @@ from services.card_tag import CardTagService from services.client import ClientService from services.service import ServiceService from services.shipping_warehouse import ShippingWarehouseService +from utils.auth import verify_user_deal_editor, verify_user_viewer class CardsService(BaseService): # region Card - @staticmethod - def grant_access(user: Union[User, dict], card_id: int): - if type(user) is User: - return - user_card_id = user['deal_id'] - if int(user_card_id) != int(card_id): - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') - async def _get_card_by_id(self, card_id) -> Union[Card, None]: return await self.session.get(Card, card_id) @@ -235,7 +229,12 @@ class CardsService(BaseService): total_items=total_items, ) - async def get_summary(self, request: GetCardSummariesRequest, pagination: PaginationSchema) -> CardSummaryResponse: + async def get_summary( + self, + request: GetCardSummariesRequest, + pagination: PaginationSchema, + user: UserUnion, + ) -> CardSummaryResponse: price_subquery = self._get_price_subquery() products_quantity_subquery = self._get_products_quantity_subquery() q = ( @@ -268,6 +267,9 @@ class CardsService(BaseService): ) ) + if isinstance(user, dict) and "client_id" in user: + q = q.where(Card.client_id == user["client_id"]) + q = self._apply_summary_filters(q, request) pagination_info = await self._summaries_pagination_info(q, pagination) @@ -346,10 +348,9 @@ class CardsService(BaseService): result.append(CardSchema.model_validate(card)) return CardGetAllResponse(cards=result) - async def get_by_id(self, user: Union[User, dict], card_id: int, return_raw=False) -> Union[ - CardSchema, Card]: - self.grant_access(user, card_id) - + async def get_by_id(self, user: UserUnion, card_id: int, return_raw=False) -> Union[ + CardSchema, Card + ]: card: Optional[Card] = await self.session.scalar( select(Card) .options( @@ -393,11 +394,17 @@ class CardsService(BaseService): ) .where(Card.id == card_id) ) - if return_raw: - return card + if not card: raise HTTPException(status_code=404, detail="Карточка не найдена") + if not verify_user_deal_editor(user, card_id, False) and \ + not verify_user_viewer(user, card.client_id, False): + raise HTTPException(status_code=starlette.status.HTTP_403_FORBIDDEN, detail='Forbidden') + + if return_raw: + return card + return CardSchema.model_validate(card) async def update_general_info( @@ -450,12 +457,15 @@ class CardsService(BaseService): await self.session.rollback() return CardUpdateGeneralInfoResponse(ok=False, message=str(e)) - async def update_products_and_services_general_info(self, request: ProductsAndServicesGeneralInfoRequest) -> ( + async def update_products_and_services_general_info(self, request: ProductsAndServicesGeneralInfoRequest, + user: UserUnion) -> ( ProductsAndServicesGeneralInfoResponse ): + verify_user_deal_editor(user, request.card_id) + card: Optional[Card] = await self.session.get(Card, request.card_id) if not card: - return ProductsAndServicesGeneralInfoRequest(ok=False, message='Карточка не найдена') + return ProductsAndServicesGeneralInfoResponse(ok=False, message='Карточка не найдена') # Updating shipping warehouse shipping_warehouse_service = ShippingWarehouseService(self.session) @@ -464,8 +474,10 @@ class CardsService(BaseService): shipping_warehouse = await shipping_warehouse_service.create_by_name(request.data.shipping_warehouse) if card.group: - for card in card.group.cards: - card.is_services_profit_accounted = request.data.is_services_profit_accounted + stmt_group = select(CardGroup).where(CardGroup.id == card.group.id).options(selectinload(CardGroup.cards)) + group = (await self.session.scalars(stmt_group)).one_or_none() + for c in group.cards: + c.is_services_profit_accounted = request.data.is_services_profit_accounted else: card.is_services_profit_accounted = request.data.is_services_profit_accounted @@ -579,7 +591,7 @@ class CardsService(BaseService): except Exception as e: return CardAddKitResponse(ok=False, message=str(e)) - def create_guest_url(self, user: User, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse: + async def create_guest_url(self, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse: access_token = AuthService(self.session).create_deal_guest_token(request.card_id) url = f"deals/{request.card_id}?accessToken={access_token}" return CardCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) @@ -668,11 +680,11 @@ class CardsService(BaseService): async def update_service_quantity( self, - user: Union[User, dict], + user: UserUnion, request: CardUpdateServiceQuantityRequest ) -> CardUpdateServiceQuantityResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) card_service = await self.session.scalar( select(CardService) .where(CardService.card_id == request.card_id, @@ -689,11 +701,11 @@ class CardsService(BaseService): async def add_service( self, - user: Union[User, dict], + user: UserUnion, request: CardAddServiceRequest ) -> CardAddServiceResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) if not card: raise HTTPException(status_code=404, detail="Карточка не найдена") @@ -723,11 +735,12 @@ class CardsService(BaseService): async def delete_service( self, - user: Union[User, dict], + user: UserUnion, request: CardDeleteServiceRequest ) -> CardDeleteServiceResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) + card_service = await self.session.scalar( select(CardService) .where(CardService.card_id == request.card_id, @@ -744,11 +757,12 @@ class CardsService(BaseService): async def delete_services( self, - user: Union[User, dict], + user: UserUnion, request: CardDeleteServicesRequest ) -> CardDeleteServicesResponse: try: - self.grant_access(user, request) + verify_user_deal_editor(user, request.card_id) + card_services = await self.session.scalars( select(CardService) .where(CardService.card_id == request.card_id, @@ -764,11 +778,11 @@ class CardsService(BaseService): async def update_service( self, - user: Union[User, dict], + user: UserUnion, request: CardUpdateServiceRequest ) -> CardUpdateServiceResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) card_service = await self.session.scalar( select(CardService) .where(CardService.card_id == request.card_id, @@ -823,11 +837,11 @@ class CardsService(BaseService): async def copy_services( self, - user: Union[User, dict], + user: UserUnion, request: CardServicesCopyRequest ) -> CardServicesCopyResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) source_services_stmt = ( select( CardProductService @@ -911,11 +925,11 @@ class CardsService(BaseService): # region Card products async def update_product_quantity( self, - user: Union[User, dict], + user: UserUnion, request: CardUpdateProductQuantityRequest ) -> CardUpdateProductQuantityResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) # check if there is no card or no product with different exceptions card_product = await self.session.scalar( select(CardProduct) @@ -933,12 +947,11 @@ class CardsService(BaseService): async def add_product( self, - user: Union[User, dict], - + user: UserUnion, request: CardAddProductRequest ) -> CardAddProductResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) if not card: @@ -978,11 +991,12 @@ class CardsService(BaseService): async def delete_product( self, - user: Union[User, dict], + user: UserUnion, request: CardDeleteProductRequest ) -> CardDeleteProductResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) + card_product = await self.session.scalar( select(CardProduct) .where(CardProduct.card_id == request.card_id, @@ -999,11 +1013,11 @@ class CardsService(BaseService): async def delete_products( self, - user: Union[User, dict], + user: UserUnion, request: CardDeleteProductsRequest ) -> CardDeleteProductsResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) card_products = await self.session.scalars( select(CardProduct) .where(CardProduct.card_id == request.card_id, @@ -1019,11 +1033,11 @@ class CardsService(BaseService): async def update_product( self, - user: Union[User, dict], + user: UserUnion, request: CardUpdateProductRequest ): try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) card_product: CardProduct = await self.session.scalar( select(CardProduct) .where(CardProduct.card_id == request.card_id, @@ -1106,11 +1120,12 @@ class CardsService(BaseService): async def add_kit_to_card_product( self, - user: Union[User, dict], + user: UserUnion, request: CardProductAddKitRequest ) -> CardProductAddKitResponse: try: - self.grant_access(user, request.card_id) + verify_user_deal_editor(user, request.card_id) + service_service = ServiceService(self.session) kit = await service_service.get_kit_by_id(request.kit_id) if not kit: diff --git a/services/card_group.py b/services/card_group.py index dac2bd4..cc94969 100644 --- a/services/card_group.py +++ b/services/card_group.py @@ -68,19 +68,17 @@ class CardGroupService(BaseService): ) if not group: return CardGroupUpdateResponse(ok=False, message="Группа не найдена") - # update by dictionary - request_dict = request.data.model_dump() - request_dict.pop("bill_request", None) update_stmt = ( update(CardGroup) .where(CardGroup.id == request.data.id) - .values(**request_dict) + .values(name=request.data.name, lexorank=request.data.lexorank) ) await self.session.execute(update_stmt) await self.session.commit() return CardGroupUpdateResponse(ok=True, message="Группа успешно обновлена") except Exception as e: + print(e) await self.session.rollback() return CardGroupUpdateResponse(ok=False, message=str(e)) diff --git a/services/chat.py b/services/chat.py index fa79ef6..96c36f8 100644 --- a/services/chat.py +++ b/services/chat.py @@ -10,8 +10,10 @@ from external.chat.chat_client import ChatClient from external.chat.schemas import * from external.kafka.services.producer_service import ProducerService from models import Message, Chat, MessageStatus, TgGroup, Client, Card, MessageFile, User +from schemas.auth import UserUnion from schemas.chat import * from services.base import BaseService +from utils.auth import verify_user_deal_editor class ChatService(BaseService): @@ -163,7 +165,9 @@ class ChatService(BaseService): return CreateChatResponse(ok=True, message='Чат успешно создан') - async def create_chat(self, request: CreateChatRequest) -> CreateChatResponse: + async def create_chat(self, request: CreateChatRequest, user: UserUnion) -> CreateChatResponse: + verify_user_deal_editor(user, request.card_id) + client: Optional[Client] = await self.session.get(Client, request.client_id) if not client: return CreateChatResponse(ok=False, message=f'Клиент с ID {request.client_id} не найден') diff --git a/services/client.py b/services/client.py index 145cc5e..fa61fe5 100644 --- a/services/client.py +++ b/services/client.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import joinedload, selectinload, noload from models import Client, ClientDetails, User, ResidualPallet, ResidualBox, ResidualProduct, Product from schemas.client import * +from services.auth import AuthService from services.base import BaseService @@ -165,3 +166,8 @@ class ClientService(BaseService): return ClientDeleteResponse(ok=True, message='Клиент удален') except Exception as e: return ClientDeleteResponse(ok=False, message=str(e)) + + async def create_guest_url(self, client_id: int) -> CreateGuestUrlResponse: + access_token = AuthService(self.session).create_client_guest_token(client_id) + url = f"leads?accessToken={access_token}" + return CreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) diff --git a/services/product.py b/services/product.py index 7ea1a37..9214420 100644 --- a/services/product.py +++ b/services/product.py @@ -1,6 +1,7 @@ from io import BytesIO from typing import Optional +import starlette.status from fastapi import HTTPException, UploadFile from sqlalchemy import select, func, Integer, update, or_ from sqlalchemy.orm import selectinload, Query @@ -10,16 +11,20 @@ from backend import config from barcodes.images_uploader import BarcodeImagesUploader from barcodes.pdf.pdf_maker import PdfMaker from external.s3_uploader.uploader import S3Uploader +from models import User from models.product import Product, ProductImage, ProductBarcodeImage +from schemas.auth import UserUnion from schemas.base import PaginationSchema from schemas.product import * from services.base import BaseService +from utils.auth import verify_user_deal_editor from utils.dependecies import is_valid_pagination class ProductService(BaseService): - async def create(self, request: ProductCreateRequest) -> ProductCreateResponse: + async def create(self, request: ProductCreateRequest, user: UserUnion) -> ProductCreateResponse: + verify_user_deal_editor(user) # Creating product product_dict = request.dict() diff --git a/services/status.py b/services/status.py index c5db979..7e198d3 100644 --- a/services/status.py +++ b/services/status.py @@ -1,8 +1,8 @@ from typing import Optional -from sqlalchemy import select, and_, func +from sqlalchemy import select, and_, func, exists, or_ -from models import CardStatus, Card +from models import CardStatus, Card, CardStatusHistory from schemas.status import * from services.base import BaseService @@ -106,6 +106,18 @@ class StatusService(BaseService): statuses[-2].is_finishing = True statuses[-1].is_finishing = False + async def _status_has_history(self, status_id: int) -> bool: + stmt = ( + select(exists(CardStatusHistory)) + .where( + or_( + CardStatusHistory.to_status_id == status_id, + CardStatusHistory.from_status_id == status_id, + ) + ) + ) + return (await self.session.scalars(stmt)).first() + async def delete_status(self, status_id: int) -> DeleteStatusResponse: status = await self._get_status_by_id(status_id) if not status: @@ -119,7 +131,8 @@ class StatusService(BaseService): await self._set_finishing_flag_to_prev_status(status) count_deals = await self._count_deals(status_id) - if count_deals == 0: + exist_in_history = await self._status_has_history(status_id) + if count_deals == 0 and not exist_in_history: await self.session.delete(status) else: status.is_deleted = True diff --git a/utils/auth.py b/utils/auth.py new file mode 100644 index 0000000..54bea6c --- /dev/null +++ b/utils/auth.py @@ -0,0 +1,46 @@ +from typing import Optional + +import starlette.status +from fastapi import HTTPException + +from constants import DEALS_VIEWER, DEAL_EDITOR +from models import User +from schemas.auth import UserUnion + + +def raise_403(): + raise HTTPException(status_code=starlette.status.HTTP_403_FORBIDDEN, detail='Forbidden') + + +def verify_user(user: UserUnion, raising: bool = True) -> bool: + if type(user) is User: + return True + if raising: + raise_403() + return False + + +def verify_user_viewer(user: UserUnion, client_id: Optional[int] = None, raising: bool = True) -> bool: + is_regular_user: bool = type(user) is User + is_viewer: bool = isinstance(user, dict) and user['sub'] == DEALS_VIEWER + if client_id is not None: + is_viewer = is_viewer and int(user['client_id']) == client_id + + if is_regular_user or is_viewer: + return True + if raising: + raise_403() + return False + + +def verify_user_deal_editor(user: UserUnion, deal_id: Optional[int] = None, raising: bool = True) -> bool: + is_regular_user: bool = type(user) is User + is_deal_editor: bool = isinstance(user, dict) and user['sub'] == DEAL_EDITOR + if deal_id is not None: + is_deal_editor = is_deal_editor and int(user['deal_id']) == deal_id + + if is_regular_user or is_deal_editor: + return True + if raising: + raise_403() + return False