feat: creatives and external channels

This commit is contained in:
Artem Tsyrulnikov
2025-11-10 15:13:38 +03:00
parent 3800c72662
commit cd167fdb43
73 changed files with 3024 additions and 947 deletions

View File

@@ -0,0 +1,19 @@
from fastapi import APIRouter
from src.controller.http.auth import auth_router
from src.controller.http.creatives import creatives_router
from src.controller.http.external_channels import external_channels_router
from src.controller.http.target_channels import target_channels_router
api_router = APIRouter()
# Public auth endpoints (no prefix)
api_router.include_router(auth_router)
# API v1 endpoints
api_v1_router = APIRouter(prefix='/api/v1')
api_v1_router.include_router(target_channels_router)
api_v1_router.include_router(external_channels_router)
api_v1_router.include_router(creatives_router)
api_router.include_router(api_v1_router)

View File

@@ -0,0 +1,17 @@
from fastapi import Depends
from fastapi.routing import APIRouter
from src import dto
from src.dependencies import get_usecase
from src.usecase import Usecase
auth_router = APIRouter(prefix='/auth', tags=['auth'])
@auth_router.get('/complete')
async def complete_auth(token: str, usecase: Usecase = Depends(get_usecase)) -> dto.ValidateLoginTokenOutput:
return await usecase.validate_login_token(
input=dto.ValidateLoginTokenInput(
token=token,
)
)

View File

@@ -0,0 +1,85 @@
import uuid
from typing import Annotated
from fastapi import Depends
from fastapi.routing import APIRouter
from src import dependencies, dto
from src.adapter.jwt import JWTPayload
creatives_router = APIRouter(prefix='/creatives', tags=['creatives'])
@creatives_router.get('')
async def list_creatives(
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
target_channel_id: uuid.UUID | None = None,
include_archived: bool = False,
) -> dto.GetCreativesOutput:
input = dto.GetCreativesInput(
user_id=current_user.user_id,
target_channel_id=target_channel_id,
include_archived=include_archived,
)
return await dependencies.get_usecase().get_creatives(input=input)
@creatives_router.get('/{creative_id}')
async def get_creative(
creative_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.CreativeOutput:
input = dto.GetCreativeInput(
creative_id=creative_id,
user_id=current_user.user_id,
)
return await dependencies.get_usecase().get_creative(input=input)
@creatives_router.post('')
async def create_creative(
request: dto.CreateCreativeInput,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.CreativeOutput:
return await dependencies.get_usecase().create_creative(request, current_user.user_id)
@creatives_router.patch('/{creative_id}')
async def update_creative(
creative_id: uuid.UUID,
request: dto.UpdateCreativeInput,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.CreativeOutput:
return await dependencies.get_usecase().update_creative(
creative_id=creative_id,
input=request,
user_id=current_user.user_id,
)
@creatives_router.post('/{creative_id}/archive')
async def archive_creative(
creative_id: uuid.UUID,
request: dto.ArchiveCreativeInput,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.CreativeOutput:
return await dependencies.get_usecase().archive_creative(
creative_id=creative_id,
input=request,
user_id=current_user.user_id,
)
@creatives_router.delete('/{creative_id}')
async def delete_creative(
creative_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> None:
input = dto.DeleteCreativeInput(
creative_id=creative_id,
user_id=current_user.user_id,
)
await dependencies.get_usecase().delete_creative(input)

View File

@@ -0,0 +1,90 @@
import uuid
from typing import Annotated
from fastapi import Depends, File, UploadFile
from fastapi.routing import APIRouter
from src import dependencies, dto
from src.adapter.jwt import JWTPayload
external_channels_router = APIRouter(prefix='/external_channels', tags=['external_channels'])
@external_channels_router.get('/target/{target_channel_id}')
async def get_external_channels(
target_channel_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.GetExternalChannelsOutput:
"""Get all external channels for a specific target channel."""
input = dto.GetExternalChannelsInput(
target_channel_id=target_channel_id,
user_id=current_user.user_id,
)
return await dependencies.get_usecase().get_external_channels(input=input)
@external_channels_router.post('')
async def create_external_channel(
request: dto.CreateExternalChannelInput,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.ExternalChannelOutput:
"""Create a new external channel and link it to target channels."""
return await dependencies.get_usecase().create_external_channel(
input=request,
user_id=current_user.user_id,
)
@external_channels_router.patch('/{channel_id}/links')
async def update_external_channel_links(
channel_id: uuid.UUID,
request: dto.UpdateExternalChannelLinksInput,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.ExternalChannelOutput:
"""
Update target channel links for an existing external channel.
You can add new target channels, remove existing ones, or both in a single request.
"""
return await dependencies.get_usecase().update_external_channel_links(
channel_id=channel_id,
input=request,
user_id=current_user.user_id,
)
@external_channels_router.delete('/{channel_id}')
async def delete_external_channel(
channel_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> None:
"""Delete an external channel."""
input = dto.DeleteExternalChannelInput(
channel_id=channel_id,
user_id=current_user.user_id,
)
await dependencies.get_usecase().delete_external_channel(input=input)
@external_channels_router.post('/import/{target_channel_id}')
async def import_external_channels(
target_channel_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
file: UploadFile = File(...),
) -> dto.ImportExternalChannelsOutput:
"""
Import external channels from Excel file (Tgstat export).
Note: This is a mock endpoint until Excel structure is clarified.
"""
file_content = await file.read()
input = dto.ImportExternalChannelsInput(
target_channel_id=target_channel_id,
user_id=current_user.user_id,
file_content=file_content,
)
return await dependencies.get_usecase().import_external_channels_from_excel(input=input)

View File

@@ -0,0 +1,34 @@
import uuid
from typing import Annotated
from fastapi import Depends
from fastapi.routing import APIRouter
from src import dependencies, dto
from src.adapter.jwt import JWTPayload
target_channels_router = APIRouter(prefix='/target_channels', tags=['target_channels'])
@target_channels_router.get('')
async def get_user_target_chans(
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.GetUserTargetChansOutput:
input = dto.GetUserTargetChansInput(
user_id=current_user.user_id,
)
return await dependencies.get_usecase().get_user_target_chans(input=input)
@target_channels_router.delete('/{channel_id}')
async def disconnect_target_chan(
channel_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> None:
input = dto.DisconnectTargetChanInput(
channel_id=channel_id,
user_id=current_user.user_id,
)
await dependencies.get_usecase().disconnect_target_chan(input=input)

View File

@@ -1,17 +0,0 @@
import uuid
from fastapi import APIRouter, Depends
from src import dto
from src.dependencies import get_usecase
from src.usecase.usecase import Usecase
user_router = APIRouter(prefix='/users', dependencies=[Depends(get_usecase)])
@user_router.get('/{user_id}')
async def get_user(
user_id: uuid.UUID,
usecase: Usecase = Depends(get_usecase),
) -> dto.GetUserOutput:
return await usecase.get_user(user_id)