feat(campaigns): delete campaign button + defer Jira to Activate
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Backend: add DELETE /campaigns/{id}?delete_tests=bool endpoint
- Backend: add delete_campaign() service — handles draft-only restriction,
  optional test deletion, nullifies child campaign FKs
- Backend: remove early Jira ticket creation from POST /campaigns,
  POST /campaigns/{id}/tests, and POST /campaigns/from-threat-actor
- Backend: activate endpoint now creates campaign Jira ticket if missing,
  then creates test tickets (all deferred from creation to activation)
- Frontend: add deleteCampaign() API function to campaigns.ts
- Frontend: two-step confirmation dialog on CampaignDetailPage —
  first confirms deletion, then asks whether to also delete associated tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 14:36:25 +02:00
parent 664210be3d
commit 2e5b47a4a2
4 changed files with 212 additions and 59 deletions

View File

@@ -425,6 +425,62 @@ def schedule_campaign(
return campaign
def delete_campaign(
db: Session,
campaign_id: str,
*,
deleter_id: uuid.UUID,
deleter_role: str,
delete_tests: bool = False,
) -> None:
"""Delete a campaign.
Only draft campaigns can be deleted unless the caller is admin.
If delete_tests=True, the associated Test objects are also deleted.
Does not commit; caller commits.
"""
campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first()
if not campaign:
raise EntityNotFoundError("Campaign", campaign_id)
if campaign.status != "draft" and deleter_role != "admin":
raise BusinessRuleViolation("Only draft campaigns can be deleted")
if str(campaign.created_by) != str(deleter_id) and deleter_role != "admin":
raise PermissionViolation("Only the creator or admin can delete this campaign")
# Collect test IDs before removing associations
campaign_tests = (
db.query(CampaignTest).filter(CampaignTest.campaign_id == campaign_id).all()
)
test_ids = [ct.test_id for ct in campaign_tests]
# Remove CampaignTest join rows (clear depends_on refs first to avoid FK cycles)
for ct in campaign_tests:
ct.depends_on = None
db.flush()
for ct in campaign_tests:
db.delete(ct)
db.flush()
# Optionally delete the associated tests
if delete_tests:
for test_id in test_ids:
test = db.query(Test).filter(Test.id == test_id).first()
if test:
db.delete(test)
db.flush()
# Null-out parent_campaign_id on child campaigns to avoid FK violation
db.query(Campaign).filter(Campaign.parent_campaign_id == campaign.id).update(
{"parent_campaign_id": None}
)
db.flush()
db.delete(campaign)
db.flush()
def get_campaign_history(db: Session, campaign_id: str) -> dict:
"""List all child campaigns (execution history) of a recurring campaign.