Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1690298
searchbale multipicker color tweaks
tiago-bd-oliveira Mar 16, 2026
d1e50a8
campaign edit page fully functional
tiago-bd-oliveira Mar 16, 2026
921657e
docker compose tweak
tiago-bd-oliveira Mar 16, 2026
8b8938a
Merge branch 'dev' of github.com:PEI-SecureLearning/core into SL-234-…
tiago-bd-oliveira Mar 16, 2026
e057ab0
sidebar tweaks
tiago-bd-oliveira Mar 16, 2026
2e39de8
entry page font fix
tiago-bd-oliveira Mar 16, 2026
49f91d5
sending profile custom header ui refactor
tiago-bd-oliveira Mar 17, 2026
ad33366
view option on sending profile password
tiago-bd-oliveira Mar 18, 2026
3bcdd01
sending profile creation summary refactor
tiago-bd-oliveira Mar 18, 2026
9a03724
SL-241
tiago-bd-oliveira Mar 18, 2026
fd51772
sonar config
tiago-bd-oliveira Mar 18, 2026
27e822b
merge with dev
tiago-bd-oliveira Mar 18, 2026
a9efad3
sonar fixes and padding adjustments
tiago-bd-oliveira Mar 18, 2026
c545dfb
asterisk component for forms with proper validation
tiago-bd-oliveira Mar 18, 2026
8e665ed
editing page fixed
tiago-bd-oliveira Mar 19, 2026
a4b191a
Merge branch 'dev' of github.com:PEI-SecureLearning/core into SL-234-…
tiago-bd-oliveira Mar 19, 2026
569385c
small sonar fix
tiago-bd-oliveira Mar 19, 2026
db75ca7
email regex validation security issue fixed
tiago-bd-oliveira Mar 19, 2026
17de472
email regex validation security issue fixed
tiago-bd-oliveira Mar 19, 2026
bf4c7e1
fix code duplication in sending profile forms
tiago-bd-oliveira Mar 19, 2026
70a95a5
Merge branch 'dev' of github.com:PEI-SecureLearning/core into SL-234-…
tiago-bd-oliveira Mar 20, 2026
54878d3
small fix
tiago-bd-oliveira Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added *
Empty file.
7 changes: 5 additions & 2 deletions api/src/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from .email_template import EmailTemplate
from .landing_page import LandingPageTemplate
from .phishing_kit import PhishingKit, PhishingKitCreate, PhishingKitDisplayInfo, CampaignPhishingKitLink, PhishingKitSendingProfileLink
from .sending_profile import SendingProfile, SendingProfileCreate, SendingProfileDisplayInfo, CampaignSendingProfileLink, CustomHeader, CustomHeaderCreate
from .campaign import Campaign, CampaignCreate, CampaignStatus, CampaignDisplayInfo, CampaignDetailInfo, CampaignGlobalStats, MIN_INTERVAL_SECONDS
from .sending_profile import SendingProfile, SendingProfileCreate, SendingProfileDisplayInfo, SendingProfileRead, CampaignSendingProfileLink, CustomHeader, CustomHeaderCreate, CustomHeaderRead
from .campaign import Campaign, CampaignCreate, CampaignStatus, CampaignDisplayInfo, CampaignDetailInfo, CampaignGlobalStats, CampaignUpdate, MIN_INTERVAL_SECONDS
from .email_sending import EmailSending, EmailSendingStatus, UserSendingInfo, RabbitMQEmailMessage, SMTPConfig
from .compliance import (
ComplianceAcceptance,
Expand Down Expand Up @@ -58,6 +58,7 @@
# Campaign
"Campaign",
"CampaignCreate",
"CampaignUpdate",
"CampaignStatus",
"CampaignDisplayInfo",
"CampaignDetailInfo",
Expand All @@ -82,6 +83,7 @@
# Custom Header
"CustomHeader",
"CustomHeaderCreate",
"CustomHeaderRead",
# Landing Page Template
"LandingPageTemplate",
# Realm
Expand All @@ -96,6 +98,7 @@
"SendingProfile",
"SendingProfileCreate",
"SendingProfileDisplayInfo",
"SendingProfileRead",
# User Group
"UserGroup",
# Compliance
Expand Down
2 changes: 2 additions & 0 deletions api/src/models/campaign/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
CampaignDisplayInfo,
CampaignDetailInfo,
CampaignGlobalStats,
CampaignUpdate,
)

__all__ = [
Expand All @@ -14,4 +15,5 @@
"CampaignDisplayInfo",
"CampaignDetailInfo",
"CampaignGlobalStats",
"CampaignUpdate",
]
14 changes: 14 additions & 0 deletions api/src/models/campaign/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ class CampaignCreate(SQLModel):
phishing_kit_ids: list[int] = []
user_group_ids: list[str]


class CampaignUpdate(SQLModel):
name: str
description: Optional[str] = None
begin_date: datetime
end_date: datetime
sending_interval_seconds: int = MIN_INTERVAL_SECONDS
sending_profile_ids: list[int] = []
phishing_kit_ids: list[int] = []
user_group_ids: list[str] = []

class CampaignDisplayInfo(SQLModel):
id: int
name: str
Expand All @@ -40,6 +51,9 @@ class CampaignDetailInfo(SQLModel):
realm_name: Optional[str] = None

# Related entities
user_group_ids: list[str] = []
phishing_kit_ids: list[int] = []
sending_profile_ids: list[int] = []
phishing_kit_names: list[str] = []
creator_id: Optional[str] = None
creator_email: Optional[str] = None
Expand Down
1 change: 1 addition & 0 deletions api/src/models/campaign/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Campaign(SQLModel, table=True):
total_clicked: int = Field(default=0)
total_phished: int = Field(default=0)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)

# FKs
realm_name: Optional[str] = Field(
Expand Down
4 changes: 3 additions & 1 deletion api/src/models/sending_profile/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from .table import SendingProfile, CampaignSendingProfileLink, CustomHeader
from .schemas import SendingProfileCreate, SendingProfileDisplayInfo, CustomHeaderCreate
from .schemas import SendingProfileCreate, SendingProfileDisplayInfo, SendingProfileRead, CustomHeaderCreate, CustomHeaderRead

__all__ = [
"SendingProfile",
"CampaignSendingProfileLink",
"CustomHeader",
"SendingProfileCreate",
"SendingProfileDisplayInfo",
"SendingProfileRead",
"CustomHeaderCreate",
"CustomHeaderRead",
]
20 changes: 20 additions & 0 deletions api/src/models/sending_profile/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,23 @@ class SendingProfileDisplayInfo(SQLModel):
from_email: str
smtp_host: Final[str]
smtp_port: Final[int]


class CustomHeaderRead(SQLModel):
id: int
name: str
value: str


class SendingProfileRead(SQLModel):
id: int
name: str
smtp_host: str
smtp_port: int
username: str
password: str
from_fname: str
from_lname: str
from_email: str
realm_name: str | None = None
custom_headers: list[CustomHeaderRead] = []
15 changes: 15 additions & 0 deletions api/src/routers/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,18 @@ def get_campaign_by_id(id: int, current_realm: CurrentRealm, session: SessionDep
def cancel_campaign(id: int, current_realm: CurrentRealm, session: SessionDep):
campaign = service.cancel_campaign(id, current_realm, session)
return {"message": f"Campaign '{campaign.name}' has been canceled"}

@router.put(
"/campaigns/{id}",
description="Update a campaign. Only for SCHEDULED campaigns.",
status_code=200,
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))],
)
def update_campaign(
id: int,
campaign_update: CampaignCreate,
current_realm: CurrentRealm,
session: SessionDep,
):
campaign = service.update_campaign(id, campaign_update, current_realm, session)
return {"message": f"Campaign '{campaign.name}' has been updated"}
21 changes: 20 additions & 1 deletion api/src/routers/org_manager/campaign_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from src.core.dependencies import SessionDep, OAuth2Scheme
from src.core.security import Roles, Resource, Scope
from src.models import PhishingKitDisplayInfo
from src.models import CampaignUpdate, PhishingKitDisplayInfo
from src.services import templates as template_service
from src.services.campaign import CampaignService
from src.services.org_manager.validation_handler import validate_realm_access
Expand Down Expand Up @@ -44,6 +44,25 @@ async def get_realm_campaign_detail(
}


@router.put(
"/{realm}/campaigns/{campaign_id}",
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))],
responses={404: {"description": "Campaign not found"}},
)
def update_realm_campaign(
realm: str,
campaign_id: int,
campaign_update: CampaignUpdate,
session: SessionDep,
token: OAuth2Scheme,
):
"""Update a scheduled campaign for the specified realm."""
validate_realm_access(token, realm)
service = CampaignService()
campaign = service.update_campaign(campaign_id, campaign_update, realm, session)
return {"message": f"Campaign '{campaign.name}' has been updated"}


@router.delete(
"/{realm}/campaigns/{campaign_id}",
status_code=status.HTTP_204_NO_CONTENT,
Expand Down
103 changes: 76 additions & 27 deletions api/src/routers/sending_profile.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from fastapi import APIRouter, Depends
from fastapi.responses import RedirectResponse, Response
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response

from src.core.security import Roles, Resource, Scope
from src.core.dependencies import CurrentRealm, SessionDep
from src.models import SendingProfile, SendingProfileCreate
from src.models import (
SendingProfileCreate,
SendingProfileDisplayInfo,
SendingProfileRead,
)
from src.services.sending_profile import SendingProfileService


Expand All @@ -12,70 +16,115 @@
service = SendingProfileService()


@router.post("/sending-profiles/test", description="Test sending profile configuration", dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))])
def test_sending_profile_configuration(
profile_data: SendingProfileCreate
):
is_valid, message = service._test_sending_profile_configuration(profile_data)
return Response(content=message, status_code=200 if is_valid else 400)
def _to_http_exception(error: Exception) -> HTTPException:
if isinstance(error, LookupError):
return HTTPException(status_code=404, detail=str(error))
if isinstance(error, ValueError):
return HTTPException(status_code=400, detail=str(error))
if isinstance(error, RuntimeError):
return HTTPException(status_code=500, detail=str(error))
return HTTPException(status_code=500, detail="Unexpected sending profile error")


@router.post(
"/sending-profiles/test",
description="Test sending profile configuration",
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))],
)
def test_sending_profile_configuration(profile_data: SendingProfileCreate):
try:
message = service.test_sending_profile_configuration(profile_data)
return Response(content=message, status_code=200)
except (LookupError, ValueError, RuntimeError) as e:
raise _to_http_exception(e)


@router.post(
"/sending-profiles", description="Create a new sending profile", status_code=201, dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))]
"/sending-profiles",
description="Create a new sending profile",
status_code=201,
response_model=SendingProfileRead,
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))],
)
def create_sending_profile(
profile_data: SendingProfileCreate,
current_realm: CurrentRealm,
session: SessionDep,
):
service.create_sending_profile(profile_data, current_realm, session)
return {"message": "Sending profile created successfully"}
try:
return service.create_sending_profile(profile_data, current_realm, session)
except (LookupError, ValueError, RuntimeError) as e:
raise _to_http_exception(e)


@router.get(
"/sending-profiles", description="Fetch all sending profiles", status_code=200, dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.VIEW))]
"/sending-profiles",
description="Fetch all sending profiles",
status_code=200,
response_model=list[SendingProfileDisplayInfo],
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.VIEW))],
)
def get_sending_profiles(
current_realm: CurrentRealm,
session: SessionDep,
):
profiles = service.get_sending_profiles_by_realm(current_realm, session)
return profiles
try:
profiles = service.get_sending_profiles_by_realm(current_realm, session)
return profiles
except (LookupError, ValueError, RuntimeError) as e:
raise _to_http_exception(e)


@router.delete(
"/sending-profiles/{id}", description="Delete a sending profile", status_code=200, dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))]
"/sending-profiles/{id}",
description="Delete a sending profile",
status_code=200,
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))],
)
def delete_sending_profile(
id: int,
session: SessionDep,
):
service.delete_sending_profile(id, session)
return {"message": "Sending profile deleted successfully"}
try:
service.delete_sending_profile(id, session)
return {"message": "Sending profile deleted successfully"}
except (LookupError, ValueError, RuntimeError) as e:
raise _to_http_exception(e)


@router.put(
"/sending-profiles/{id}", description="Update a sending profile", status_code=200, dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))]
"/sending-profiles/{id}",
description="Update a sending profile",
status_code=200,
response_model=SendingProfileRead,
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.MANAGE))],
)
def update_sending_profile(
id: int,
profile_data: SendingProfileCreate,
session: SessionDep,
):
updated_profile = service.update_sending_profile(id, profile_data, session)
if not updated_profile:
return {"message": "Sending profile not found"}
return {"message": "Sending profile updated successfully"}
try:
updated_profile = service.update_sending_profile(id, profile_data, session)
return updated_profile
except (LookupError, ValueError, RuntimeError) as e:
raise _to_http_exception(e)


@router.get(
"/sending-profiles/{id}", description="Fetch sending profile by ID", status_code=200, dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.VIEW))]
"/sending-profiles/{id}",
description="Fetch sending profile by ID",
status_code=200,
response_model=SendingProfileRead,
dependencies=[Depends(Roles(Resource.ORG_MANAGER, Scope.VIEW))],
responses={404: {"description": "Sending profile not found"}},
)
def get_sending_profile_by_id(
id: int,
session: SessionDep,
):
profile = service.get_sending_profile(id, session)
if not profile:
return {"message": "Sending profile not found"}
try:
profile = service.get_sending_profile(id, session)
except (LookupError, ValueError, RuntimeError) as e:
raise _to_http_exception(e)
return profile
5 changes: 4 additions & 1 deletion api/src/services/campaign/campaign_handler.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import datetime
import math
from fastapi import HTTPException
from sqlmodel import Session, select

from src.models import (
Campaign,
CampaignCreate,
CampaignUpdate,
CampaignStatus,
EmailSendingStatus,
MIN_INTERVAL_SECONDS,
Expand Down Expand Up @@ -107,7 +109,7 @@ def cancel_campaign(
def update_campaign(
self,
id: int,
campaign_update: CampaignCreate,
campaign_update: CampaignUpdate,
current_realm: str,
session: Session,
) -> Campaign:
Expand Down Expand Up @@ -145,6 +147,7 @@ def update_campaign(
campaign_update.sending_profile_ids,
)

campaign.updated_at = datetime.now()
session.commit()
session.refresh(campaign)

Expand Down
3 changes: 3 additions & 0 deletions api/src/services/campaign/stats_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ def _to_detail_info(self, campaign: Campaign) -> CampaignDetailInfo:
sending_interval_seconds=campaign.sending_interval_seconds,
status=campaign.status,
realm_name=campaign.realm_name,
user_group_ids=[group.id for group in campaign.user_groups],
phishing_kit_ids=[kit.id for kit in campaign.phishing_kits],
sending_profile_ids=[profile.id for profile in campaign.sending_profiles],
sending_profile_names=[p.name for p in campaign.sending_profiles],
phishing_kit_names=[
kit.name for kit in campaign.phishing_kits
Expand Down
Loading
Loading