diff --git a/backend/routers/backlink_outreach.py b/backend/routers/backlink_outreach.py index 04009c60..a57caa1f 100644 --- a/backend/routers/backlink_outreach.py +++ b/backend/routers/backlink_outreach.py @@ -22,7 +22,10 @@ from services.backlink_outreach_models import ( SuppressionAddRequest, ) from services.backlink_outreach_service import backlink_outreach_service -from services.backlink_outreach_storage import BacklinkOutreachStorageService +from services.backlink_outreach_storage import ( + BacklinkCampaignNotFoundError, + BacklinkOutreachStorageService, +) from services.backlink_outreach_sender import backlink_outreach_sender from services.backlink_outreach_reply_monitor import backlink_outreach_reply_monitor from services.backlink_outreach_template_generator import ( @@ -87,9 +90,14 @@ async def discover_deep_backlink_opportunities( ): """Enhanced discovery using Exa neural search + DuckDuckGo with full-page scraping.""" user_id = _resolve_user_id(current_user) - result = await backlink_outreach_service.deep_discover(payload.keyword, payload.max_results) + storage = None if payload.campaign_id: storage = BacklinkOutreachStorageService() + if not storage.get_campaign(payload.campaign_id, user_id): + raise HTTPException(status_code=404, detail="Campaign not found") + + result = await backlink_outreach_service.deep_discover(payload.keyword, payload.max_results) + if payload.campaign_id: saved = 0 save_failed = 0 for opp in result.get("opportunities", []): @@ -183,7 +191,9 @@ async def add_campaign_lead( notes=payload.notes, ) return lead - except Exception as e: + except BacklinkCampaignNotFoundError: + raise HTTPException(status_code=404, detail="Campaign not found") + except Exception: raise HTTPException(status_code=500, detail="Failed to add lead") diff --git a/backend/services/backlink_outreach_storage.py b/backend/services/backlink_outreach_storage.py index 0f41ce00..a19311e1 100644 --- a/backend/services/backlink_outreach_storage.py +++ b/backend/services/backlink_outreach_storage.py @@ -16,6 +16,10 @@ from models.backlink_outreach_models import ( ) +class BacklinkCampaignNotFoundError(RuntimeError): + """Raised when a backlink campaign is missing or not owned by the user.""" + + class BacklinkOutreachStorageService: _NEW_LEAD_COLUMNS = [ "url", "page_title", "snippet", "confidence_score", "discovery_source", "notes" @@ -120,6 +124,14 @@ class BacklinkOutreachStorageService: # -- Lead CRUD -- + def _campaign_belongs_to_user(self, db, campaign_id: str, user_id: str) -> bool: + return ( + db.query(BacklinkCampaign) + .filter(BacklinkCampaign.id == campaign_id, BacklinkCampaign.user_id == user_id) + .first() + is not None + ) + def add_lead( self, campaign_id: str, @@ -138,6 +150,9 @@ class BacklinkOutreachStorageService: if not db: raise RuntimeError("Database session unavailable") try: + if not self._campaign_belongs_to_user(db, campaign_id, user_id): + raise BacklinkCampaignNotFoundError("Campaign not found") + lead = BacklinkLead( id=f"bl_{uuid4().hex[:16]}", campaign_id=campaign_id, @@ -164,6 +179,9 @@ class BacklinkOutreachStorageService: if not db: raise RuntimeError("Database session unavailable") try: + if not self._campaign_belongs_to_user(db, campaign_id, user_id): + raise BacklinkCampaignNotFoundError("Campaign not found") + added = [] for data in leads_data: lead = BacklinkLead(