376 lines
10 KiB
Python
376 lines
10 KiB
Python
"""
|
|
ⒸAngelaMos | 2025
|
|
service.py
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.exceptions import (
|
|
AssetNotFound,
|
|
NotProgramOwner,
|
|
ProgramNotFound,
|
|
SlugAlreadyExists,
|
|
)
|
|
from user.User import User
|
|
from .schemas import (
|
|
AssetCreate,
|
|
AssetResponse,
|
|
AssetUpdate,
|
|
ProgramCreate,
|
|
ProgramDetailResponse,
|
|
ProgramListResponse,
|
|
ProgramResponse,
|
|
ProgramUpdate,
|
|
RewardTierCreate,
|
|
RewardTierResponse,
|
|
)
|
|
from .repository import (
|
|
AssetRepository,
|
|
ProgramRepository,
|
|
RewardTierRepository,
|
|
)
|
|
|
|
|
|
class ProgramService:
|
|
"""
|
|
Business logic for program operations
|
|
"""
|
|
def __init__(self, session: AsyncSession) -> None:
|
|
self.session = session
|
|
|
|
async def create_program(
|
|
self,
|
|
user: User,
|
|
program_data: ProgramCreate,
|
|
) -> ProgramResponse:
|
|
"""
|
|
Create a new bug bounty program
|
|
"""
|
|
if await ProgramRepository.slug_exists(self.session,
|
|
program_data.slug):
|
|
raise SlugAlreadyExists(program_data.slug)
|
|
|
|
program = await ProgramRepository.create(
|
|
self.session,
|
|
company_id = user.id,
|
|
name = program_data.name,
|
|
slug = program_data.slug,
|
|
description = program_data.description,
|
|
rules = program_data.rules,
|
|
response_sla_hours = program_data.response_sla_hours,
|
|
visibility = program_data.visibility,
|
|
)
|
|
return ProgramResponse.model_validate(program)
|
|
|
|
async def get_program_by_slug(
|
|
self,
|
|
slug: str,
|
|
) -> ProgramDetailResponse:
|
|
"""
|
|
Get program by slug with full details
|
|
"""
|
|
program = await ProgramRepository.get_by_slug_with_details(
|
|
self.session,
|
|
slug
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(slug)
|
|
|
|
return ProgramDetailResponse(
|
|
id = program.id,
|
|
created_at = program.created_at,
|
|
updated_at = program.updated_at,
|
|
company_id = program.company_id,
|
|
name = program.name,
|
|
slug = program.slug,
|
|
description = program.description,
|
|
rules = program.rules,
|
|
response_sla_hours = program.response_sla_hours,
|
|
status = program.status,
|
|
visibility = program.visibility,
|
|
assets = [
|
|
AssetResponse.model_validate(a) for a in program.assets
|
|
],
|
|
reward_tiers = [
|
|
RewardTierResponse.model_validate(r)
|
|
for r in program.reward_tiers
|
|
],
|
|
)
|
|
|
|
async def get_program_by_id(
|
|
self,
|
|
program_id: UUID,
|
|
) -> ProgramResponse:
|
|
"""
|
|
Get program by ID
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
return ProgramResponse.model_validate(program)
|
|
|
|
async def list_public_programs(
|
|
self,
|
|
page: int,
|
|
size: int,
|
|
) -> ProgramListResponse:
|
|
"""
|
|
List active public programs
|
|
"""
|
|
skip = (page - 1) * size
|
|
programs = await ProgramRepository.get_public_programs(
|
|
self.session,
|
|
skip = skip,
|
|
limit = size,
|
|
)
|
|
total = await ProgramRepository.count_public_programs(self.session)
|
|
return ProgramListResponse(
|
|
items = [ProgramResponse.model_validate(p) for p in programs],
|
|
total = total,
|
|
page = page,
|
|
size = size,
|
|
)
|
|
|
|
async def list_my_programs(
|
|
self,
|
|
user: User,
|
|
page: int,
|
|
size: int,
|
|
) -> ProgramListResponse:
|
|
"""
|
|
List programs owned by user
|
|
"""
|
|
skip = (page - 1) * size
|
|
programs = await ProgramRepository.get_by_company(
|
|
self.session,
|
|
company_id = user.id,
|
|
skip = skip,
|
|
limit = size,
|
|
)
|
|
total = await ProgramRepository.count_by_company(
|
|
self.session,
|
|
user.id
|
|
)
|
|
return ProgramListResponse(
|
|
items = [ProgramResponse.model_validate(p) for p in programs],
|
|
total = total,
|
|
page = page,
|
|
size = size,
|
|
)
|
|
|
|
async def update_program(
|
|
self,
|
|
user: User,
|
|
program_id: UUID,
|
|
program_data: ProgramUpdate,
|
|
) -> ProgramResponse:
|
|
"""
|
|
Update program details
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
if program.company_id != user.id:
|
|
raise NotProgramOwner()
|
|
|
|
update_dict = program_data.model_dump(exclude_unset = True)
|
|
updated = await ProgramRepository.update(
|
|
self.session,
|
|
program,
|
|
**update_dict,
|
|
)
|
|
return ProgramResponse.model_validate(updated)
|
|
|
|
async def delete_program(
|
|
self,
|
|
user: User,
|
|
program_id: UUID,
|
|
) -> None:
|
|
"""
|
|
Delete a program
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
if program.company_id != user.id:
|
|
raise NotProgramOwner()
|
|
|
|
await ProgramRepository.delete(self.session, program)
|
|
|
|
async def add_asset(
|
|
self,
|
|
user: User,
|
|
program_id: UUID,
|
|
asset_data: AssetCreate,
|
|
) -> AssetResponse:
|
|
"""
|
|
Add asset to program scope
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
if program.company_id != user.id:
|
|
raise NotProgramOwner()
|
|
|
|
asset = await AssetRepository.create(
|
|
self.session,
|
|
program_id = program_id,
|
|
asset_type = asset_data.asset_type,
|
|
identifier = asset_data.identifier,
|
|
in_scope = asset_data.in_scope,
|
|
description = asset_data.description,
|
|
)
|
|
return AssetResponse.model_validate(asset)
|
|
|
|
async def update_asset(
|
|
self,
|
|
user: User,
|
|
program_id: UUID,
|
|
asset_id: UUID,
|
|
asset_data: AssetUpdate,
|
|
) -> AssetResponse:
|
|
"""
|
|
Update an asset
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
if program.company_id != user.id:
|
|
raise NotProgramOwner()
|
|
|
|
asset = await AssetRepository.get_by_id(self.session, asset_id)
|
|
if not asset or asset.program_id != program_id:
|
|
raise AssetNotFound(str(asset_id))
|
|
|
|
update_dict = asset_data.model_dump(exclude_unset = True)
|
|
updated = await AssetRepository.update(
|
|
self.session,
|
|
asset,
|
|
**update_dict,
|
|
)
|
|
return AssetResponse.model_validate(updated)
|
|
|
|
async def delete_asset(
|
|
self,
|
|
user: User,
|
|
program_id: UUID,
|
|
asset_id: UUID,
|
|
) -> None:
|
|
"""
|
|
Delete an asset
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
if program.company_id != user.id:
|
|
raise NotProgramOwner()
|
|
|
|
asset = await AssetRepository.get_by_id(self.session, asset_id)
|
|
if not asset or asset.program_id != program_id:
|
|
raise AssetNotFound(str(asset_id))
|
|
|
|
await AssetRepository.delete(self.session, asset)
|
|
|
|
async def list_assets(
|
|
self,
|
|
program_id: UUID,
|
|
) -> list[AssetResponse]:
|
|
"""
|
|
List all assets for a program
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
assets = await AssetRepository.get_by_program(
|
|
self.session,
|
|
program_id
|
|
)
|
|
return [AssetResponse.model_validate(a) for a in assets]
|
|
|
|
async def set_reward_tiers(
|
|
self,
|
|
user: User,
|
|
program_id: UUID,
|
|
tiers: list[RewardTierCreate],
|
|
) -> list[RewardTierResponse]:
|
|
"""
|
|
Set reward tiers for a program (replaces existing)
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
if program.company_id != user.id:
|
|
raise NotProgramOwner()
|
|
|
|
await RewardTierRepository.delete_by_program(
|
|
self.session,
|
|
program_id
|
|
)
|
|
|
|
created_tiers = []
|
|
for tier_data in tiers:
|
|
tier = await RewardTierRepository.create(
|
|
self.session,
|
|
program_id = program_id,
|
|
severity = tier_data.severity,
|
|
min_bounty = tier_data.min_bounty,
|
|
max_bounty = tier_data.max_bounty,
|
|
currency = tier_data.currency,
|
|
)
|
|
created_tiers.append(RewardTierResponse.model_validate(tier))
|
|
|
|
return created_tiers
|
|
|
|
async def list_reward_tiers(
|
|
self,
|
|
program_id: UUID,
|
|
) -> list[RewardTierResponse]:
|
|
"""
|
|
List reward tiers for a program
|
|
"""
|
|
program = await ProgramRepository.get_by_id(
|
|
self.session,
|
|
program_id
|
|
)
|
|
if not program:
|
|
raise ProgramNotFound(str(program_id))
|
|
|
|
tiers = await RewardTierRepository.get_by_program(
|
|
self.session,
|
|
program_id
|
|
)
|
|
return [RewardTierResponse.model_validate(t) for t in tiers]
|