mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-05-25 09:14:18 +02:00
Added webhook data level and events filtering
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -40,3 +40,35 @@ var Events = []string{
|
||||
EVENT_CAMPAIGN_RECIPIENT_REPORTED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_CANCELLED,
|
||||
}
|
||||
|
||||
// webhook event bit flags for storing selected events as int
|
||||
// includes all events that call HandleWebhook (campaign.go + proxy.go)
|
||||
const (
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_CLOSED = 1 << 0 // 1
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_SENT = 1 << 1 // 2
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED = 1 << 2 // 4
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_READ = 1 << 3 // 8
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA = 1 << 4 // 16
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED = 1 << 5 // 32
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED = 1 << 6 // 64
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_PAGE_VISITED = 1 << 7 // 128
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED = 1 << 8 // 256
|
||||
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED = 1 << 9 // 512
|
||||
)
|
||||
|
||||
// WEBHOOK_EVENT_ALL_BITS represents all events selected
|
||||
const WEBHOOK_EVENT_ALL_BITS = 1023 // 2^10 - 1
|
||||
|
||||
// map event names to their bit positions
|
||||
var WebhookEventToBit = map[string]int{
|
||||
EVENT_CAMPAIGN_CLOSED: WEBHOOK_EVENT_BIT_CAMPAIGN_CLOSED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_SENT,
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_READ,
|
||||
EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA,
|
||||
EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,21 @@ type Campaign struct {
|
||||
IsAnonymous bool `gorm:"not null;default:false"`
|
||||
IsTest bool `gorm:"not null;default:false"`
|
||||
Obfuscate bool `gorm:"not null;default:false"`
|
||||
WebhookIncludeData bool `gorm:"not null;default:false"`
|
||||
WebhookIncludeData string `gorm:"not null;default:'full'"`
|
||||
// WebhookEvents is a binary format storing selected events as bits (10 events)
|
||||
// 0 = all events (default, backward compatible)
|
||||
// non-zero = only selected events trigger webhooks
|
||||
// bit 0 (1): campaign_closed
|
||||
// bit 1 (2): campaign_recipient_message_sent
|
||||
// bit 2 (4): campaign_recipient_message_failed
|
||||
// bit 3 (8): campaign_recipient_message_read
|
||||
// bit 4 (16): campaign_recipient_submitted_data
|
||||
// bit 5 (32): campaign_recipient_evasion_page_visited
|
||||
// bit 6 (64): campaign_recipient_before_page_visited
|
||||
// bit 7 (128): campaign_recipient_page_visited
|
||||
// bit 8 (256): campaign_recipient_after_page_visited
|
||||
// bit 9 (512): campaign_recipient_deny_page_visited
|
||||
WebhookEvents int `gorm:"not null;default:0"`
|
||||
|
||||
// has one
|
||||
CampaignTemplateID *uuid.UUID `gorm:"index;type:uuid;"`
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
"github.com/phishingclub/phishingclub/errs"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
"github.com/phishingclub/phishingclub/validate"
|
||||
@@ -42,7 +43,8 @@ type Campaign struct {
|
||||
IsAnonymous nullable.Nullable[bool] `json:"isAnonymous"`
|
||||
IsTest nullable.Nullable[bool] `json:"isTest"`
|
||||
Obfuscate nullable.Nullable[bool] `json:"obfuscate"`
|
||||
WebhookIncludeData nullable.Nullable[bool] `json:"webhookIncludeData"`
|
||||
WebhookIncludeData nullable.Nullable[string] `json:"webhookIncludeData"`
|
||||
WebhookEvents nullable.Nullable[int] `json:"webhookEvents"`
|
||||
TemplateID nullable.Nullable[uuid.UUID] `json:"templateID"`
|
||||
Template *CampaignTemplate `json:"template"`
|
||||
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
|
||||
@@ -63,6 +65,53 @@ type Campaign struct {
|
||||
}
|
||||
|
||||
// Validate checks if the campaign has a valid state
|
||||
const (
|
||||
WebhookDataLevelNone = "none"
|
||||
WebhookDataLevelBasic = "basic"
|
||||
WebhookDataLevelFull = "full"
|
||||
)
|
||||
|
||||
// WebhookEventsToBinary converts array of event names to bitwise int
|
||||
func WebhookEventsToBinary(events []string) int {
|
||||
if len(events) == 0 {
|
||||
return 0 // 0 means all events
|
||||
}
|
||||
binary := 0
|
||||
for _, event := range events {
|
||||
if bit, ok := data.WebhookEventToBit[event]; ok {
|
||||
binary |= bit
|
||||
}
|
||||
}
|
||||
return binary
|
||||
}
|
||||
|
||||
// WebhookEventsFromBinary converts bitwise int to array of event names
|
||||
func WebhookEventsFromBinary(binary int) []string {
|
||||
if binary == 0 {
|
||||
return []string{} // empty array means all events
|
||||
}
|
||||
events := []string{}
|
||||
for eventName, bit := range data.WebhookEventToBit {
|
||||
if binary&bit != 0 {
|
||||
events = append(events, eventName)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// IsWebhookEventEnabled checks if an event should trigger webhook
|
||||
func IsWebhookEventEnabled(webhookEvents int, eventName string) bool {
|
||||
// 0 means all events enabled (backward compatible)
|
||||
if webhookEvents == 0 {
|
||||
return true
|
||||
}
|
||||
// check if the event bit is set
|
||||
if bit, ok := data.WebhookEventToBit[eventName]; ok {
|
||||
return webhookEvents&bit != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Campaign) Validate() error {
|
||||
if err := validate.NullableFieldRequired("name", c.Name); err != nil {
|
||||
return err
|
||||
@@ -216,6 +265,32 @@ func (c *Campaign) Validate() error {
|
||||
c.NotableEventID.SetNull()
|
||||
}
|
||||
|
||||
// validate webhookIncludeData enum
|
||||
if c.WebhookIncludeData.IsSpecified() {
|
||||
if level, err := c.WebhookIncludeData.Get(); err == nil {
|
||||
if level != WebhookDataLevelNone &&
|
||||
level != WebhookDataLevelBasic &&
|
||||
level != WebhookDataLevelFull {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("must be 'none', 'basic', or 'full'"),
|
||||
"webhookIncludeData",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate webhookEvents is non-negative
|
||||
if c.WebhookEvents.IsSpecified() && !c.WebhookEvents.IsNull() {
|
||||
if events, err := c.WebhookEvents.Get(); err == nil {
|
||||
if events < 0 {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("must be non-negative"),
|
||||
"webhookEvents",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -379,11 +454,17 @@ func (c *Campaign) ToDBMap() map[string]any {
|
||||
}
|
||||
}
|
||||
if c.WebhookIncludeData.IsSpecified() {
|
||||
m["webhook_include_data"] = false
|
||||
m["webhook_include_data"] = WebhookDataLevelFull
|
||||
if v, err := c.WebhookIncludeData.Get(); err == nil {
|
||||
m["webhook_include_data"] = v
|
||||
}
|
||||
}
|
||||
if c.WebhookEvents.IsSpecified() {
|
||||
m["webhook_events"] = 0
|
||||
if v, err := c.WebhookEvents.Get(); err == nil {
|
||||
m["webhook_events"] = v
|
||||
}
|
||||
}
|
||||
if c.TemplateID.IsSpecified() {
|
||||
m["campaign_template_id"] = nil
|
||||
if v, err := c.TemplateID.Get(); err == nil {
|
||||
|
||||
@@ -1488,6 +1488,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
|
||||
isTest := nullable.NewNullableWithValue(row.IsTest)
|
||||
obfuscate := nullable.NewNullableWithValue(row.Obfuscate)
|
||||
webhookIncludeData := nullable.NewNullableWithValue(row.WebhookIncludeData)
|
||||
webhookEvents := nullable.NewNullableWithValue(row.WebhookEvents)
|
||||
var templateID nullable.Nullable[uuid.UUID]
|
||||
if row.CampaignTemplateID != nil {
|
||||
templateID = nullable.NewNullableWithValue(*row.CampaignTemplateID)
|
||||
@@ -1617,6 +1618,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
|
||||
IsTest: isTest,
|
||||
Obfuscate: obfuscate,
|
||||
WebhookIncludeData: webhookIncludeData,
|
||||
WebhookEvents: webhookEvents,
|
||||
TemplateID: templateID,
|
||||
Template: template,
|
||||
RecipientGroups: recipientGroups,
|
||||
|
||||
+48
-11
@@ -77,6 +77,14 @@ func (c *Campaign) Create(
|
||||
if len(campaign.RecipientGroupIDs) == 0 {
|
||||
return nil, validate.WrapErrorWithField(errors.New("no groups provided"), "Recipient Groups")
|
||||
}
|
||||
// set default webhookIncludeData for backward compatibility
|
||||
if !campaign.WebhookIncludeData.IsSpecified() {
|
||||
campaign.WebhookIncludeData.Set(model.WebhookDataLevelFull)
|
||||
}
|
||||
// set default webhookEvents (0 means all events) for backward compatibility
|
||||
if !campaign.WebhookEvents.IsSpecified() {
|
||||
campaign.WebhookEvents.Set(0)
|
||||
}
|
||||
// if the schedule type is scheduled, set the start time to start of day and end to the end of the last day
|
||||
if campaign.ConstraintWeekDays.IsSpecified() && !campaign.ConstraintWeekDays.IsNull() {
|
||||
if err := campaign.ValidateSendTimesSet(); err != nil {
|
||||
@@ -1338,6 +1346,12 @@ func (c *Campaign) UpdateByID(
|
||||
if v, err := incoming.WebhookIncludeData.Get(); err == nil {
|
||||
current.WebhookIncludeData.Set(v)
|
||||
}
|
||||
if v, err := incoming.WebhookEvents.Get(); err == nil {
|
||||
current.WebhookEvents.Set(v)
|
||||
}
|
||||
if v, err := incoming.TemplateID.Get(); err == nil {
|
||||
current.TemplateID.Set(v)
|
||||
}
|
||||
if v, err := incoming.SortField.Get(); err == nil {
|
||||
current.SortField.Set(v)
|
||||
}
|
||||
@@ -3149,27 +3163,50 @@ func (c *Campaign) HandleWebhook(
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// check if campaign has webhookIncludeData enabled
|
||||
// check campaign webhook data level
|
||||
campaign, err := c.CampaignRepository.GetByID(ctx, campaignID, &repository.CampaignOption{})
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
// only include captured data if webhookIncludeData is enabled
|
||||
var dataToSend map[string]interface{}
|
||||
if campaign.WebhookIncludeData.MustGet() {
|
||||
dataToSend = capturedData
|
||||
// check if this event should trigger webhook notification
|
||||
webhookEvents := 0
|
||||
if events, err := campaign.WebhookEvents.Get(); err == nil {
|
||||
webhookEvents = events
|
||||
}
|
||||
// 0 means all events (backward compatibility)
|
||||
// otherwise check if the event bit is set
|
||||
if !model.IsWebhookEventEnabled(webhookEvents, eventName) {
|
||||
// event not in selected events, skip webhook
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine what data to send based on webhookIncludeData level
|
||||
dataLevel := model.WebhookDataLevelFull
|
||||
if level, err := campaign.WebhookIncludeData.Get(); err == nil {
|
||||
dataLevel = level
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
webhookReq := WebhookRequest{
|
||||
Time: &now,
|
||||
CampaignName: campaignName,
|
||||
Event: eventName,
|
||||
Data: dataToSend,
|
||||
Time: &now,
|
||||
Event: eventName,
|
||||
}
|
||||
if email != nil {
|
||||
webhookReq.Email = email.String()
|
||||
|
||||
// apply data level filters
|
||||
switch dataLevel {
|
||||
case model.WebhookDataLevelNone:
|
||||
// only time and event - no campaign name, no email, no data
|
||||
case model.WebhookDataLevelBasic:
|
||||
// include campaign name but no email or captured data
|
||||
webhookReq.CampaignName = campaignName
|
||||
case model.WebhookDataLevelFull:
|
||||
// include everything
|
||||
webhookReq.CampaignName = campaignName
|
||||
webhookReq.Data = capturedData
|
||||
if email != nil {
|
||||
webhookReq.Email = email.String()
|
||||
}
|
||||
}
|
||||
// the webhook is handles as a different go routine
|
||||
// so we don't block the campaign handling thread
|
||||
|
||||
@@ -386,10 +386,33 @@ func (w *Webhook) Send(
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WebhookRequest represents the payload sent to webhook endpoints.
|
||||
// webhooks are sent based on the campaign's webhookEvents setting (stored as bitwise int):
|
||||
// - 0: all events trigger webhooks (default, backward compatible)
|
||||
// - non-zero: only events with their bit set trigger webhooks
|
||||
//
|
||||
// webhook events (10 total - events that call HandleWebhook):
|
||||
// from campaign.go (4 events):
|
||||
// - campaign_closed: when a campaign finishes
|
||||
// - campaign_recipient_message_sent: when an email is successfully sent
|
||||
// - campaign_recipient_message_failed: when an email fails to send
|
||||
// - campaign_recipient_message_read: when tracking pixel is loaded
|
||||
// from proxy.go (6 events):
|
||||
// - campaign_recipient_submitted_data: when user submits data on phishing page
|
||||
// - campaign_recipient_evasion_page_visited: when evasion page is visited
|
||||
// - campaign_recipient_before_page_visited: when before page is visited
|
||||
// - campaign_recipient_page_visited: when landing page is visited
|
||||
// - campaign_recipient_after_page_visited: when after page is visited
|
||||
// - campaign_recipient_deny_page_visited: when deny page is visited
|
||||
//
|
||||
// the fields included depend on the campaign's webhookIncludeData setting:
|
||||
// - "none": only Time and Event are sent (maximum privacy)
|
||||
// - "basic": Time, Event, and CampaignName are sent (no PII)
|
||||
// - "full": all fields including Email and Data are sent (complete information)
|
||||
type WebhookRequest struct {
|
||||
Time *time.Time `json:"time"`
|
||||
CampaignName string `json:"campaignName"`
|
||||
Email string `json:"email"`
|
||||
CampaignName string `json:"campaignName,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Event string `json:"event"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
@@ -512,7 +512,8 @@ export class API {
|
||||
* @param {string} campaign.denyPageID uuid
|
||||
* @param {string} campaign.evasionPageID uuid
|
||||
* @param {string} campaign.webhookID uuid
|
||||
* @param {boolean} [campaign.webhookIncludeData]
|
||||
* @param {string} [campaign.webhookIncludeData]
|
||||
* @param {number} [campaign.webhookEvents]
|
||||
* @param {Array} [campaign.constraintWeekDays]
|
||||
* @param {string} [campaign.constraintStartTime]
|
||||
* @param {string} [campaign.constraintEndTime]
|
||||
@@ -541,6 +542,7 @@ export class API {
|
||||
evasionPageID,
|
||||
webhookID,
|
||||
webhookIncludeData,
|
||||
webhookEvents,
|
||||
constraintWeekDays,
|
||||
constraintStartTime,
|
||||
constraintEndTime,
|
||||
@@ -568,6 +570,7 @@ export class API {
|
||||
evasionPageID,
|
||||
webhookID,
|
||||
webhookIncludeData,
|
||||
webhookEvents,
|
||||
constraintWeekDays,
|
||||
constraintStartTime,
|
||||
constraintEndTime,
|
||||
@@ -597,7 +600,8 @@ export class API {
|
||||
* @param {string} campaign.denyPageID uuid
|
||||
* @param {string} campaign.evasionPageID uuid
|
||||
* @param {string} campaign.webhookID uuid
|
||||
* @param {boolean} [campaign.webhookIncludeData]
|
||||
* @param {string} [campaign.webhookIncludeData]
|
||||
* @param {number} [campaign.webhookEvents]
|
||||
* @param {Array} [campaign.constraintWeekDays]
|
||||
* @param {string} [campaign.constraintStartTime]
|
||||
* @param {string} [campaign.constraintEndTime]
|
||||
@@ -626,6 +630,7 @@ export class API {
|
||||
evasionPageID,
|
||||
webhookID,
|
||||
webhookIncludeData,
|
||||
webhookEvents,
|
||||
constraintWeekDays,
|
||||
constraintStartTime,
|
||||
constraintEndTime,
|
||||
@@ -652,6 +657,7 @@ export class API {
|
||||
evasionPageID,
|
||||
webhookID,
|
||||
webhookIncludeData,
|
||||
webhookEvents,
|
||||
constraintWeekDays,
|
||||
constraintStartTime,
|
||||
constraintEndTime,
|
||||
|
||||
@@ -413,18 +413,19 @@
|
||||
{/if}
|
||||
<!-- Selected items -->
|
||||
{#if value.length > 0}
|
||||
<div class="flex flex-row flex-wrap mt-4 gap-2">
|
||||
<div class="flex flex-row flex-wrap mt-4 gap-2 max-h-48 overflow-y-auto">
|
||||
{#each value as option}
|
||||
<button
|
||||
type="button"
|
||||
on:click|preventDefault={removeSelectedOption}
|
||||
on:keypress|preventDefault={removeSelectedOption}
|
||||
data-value={option}
|
||||
class="flex flex-row items-center bg-gray-100 dark:bg-gray-800/60 hover:bg-gray-200 dark:hover:bg-gray-700/80 px-2 py-1 rounded-md text-gray-900 dark:text-gray-300 text-sm transition-colors duration-200"
|
||||
class="flex flex-row items-center bg-gray-100 dark:bg-gray-800/60 hover:bg-gray-200 dark:hover:bg-gray-700/80 px-2 py-1 rounded-md text-gray-900 dark:text-gray-300 text-xs transition-colors duration-200 flex-shrink-0"
|
||||
aria-label="Remove {option}"
|
||||
style="max-width: 280px;"
|
||||
>
|
||||
{option}
|
||||
<img class="w-3 ml-2 pointer-events-none" src="/delete2.svg" alt="" />
|
||||
<span class="truncate block" style="max-width: 250px;">{option}</span>
|
||||
<img class="w-3 ml-1 pointer-events-none flex-shrink-0" src="/delete2.svg" alt="" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -130,6 +130,92 @@
|
||||
}
|
||||
];
|
||||
|
||||
// webhook data level descriptions:
|
||||
// - none: only send event type and timestamp - no campaign info, emails, or data
|
||||
// - basic: send campaign name and event, but exclude emails and submission data
|
||||
// - full: include all information: emails, captured credentials, and submission data
|
||||
const webhookDataLevelOptions = [
|
||||
{
|
||||
label: 'Minimum',
|
||||
value: 'none'
|
||||
},
|
||||
{
|
||||
label: 'Basic',
|
||||
value: 'basic'
|
||||
},
|
||||
{
|
||||
label: 'Full',
|
||||
value: 'full'
|
||||
}
|
||||
];
|
||||
|
||||
// webhook event options - 0 means all events will trigger webhook (default)
|
||||
const webhookEventOptions = [
|
||||
'campaign_recipient_message_sent',
|
||||
'campaign_recipient_message_failed',
|
||||
'campaign_recipient_message_read',
|
||||
'campaign_recipient_submitted_data',
|
||||
'campaign_recipient_page_visited',
|
||||
'campaign_recipient_before_page_visited',
|
||||
'campaign_recipient_after_page_visited',
|
||||
'campaign_recipient_evasion_page_visited',
|
||||
'campaign_recipient_deny_page_visited',
|
||||
'campaign_closed'
|
||||
];
|
||||
|
||||
// human-readable display names for webhook events
|
||||
const webhookEventDisplayNames = {
|
||||
campaign_closed: 'Campaign Closed',
|
||||
campaign_recipient_message_sent: 'Message Sent',
|
||||
campaign_recipient_message_failed: 'Message Failed',
|
||||
campaign_recipient_message_read: 'Message Read',
|
||||
campaign_recipient_submitted_data: 'Submitted Data',
|
||||
campaign_recipient_evasion_page_visited: 'Evasion Page Visited',
|
||||
campaign_recipient_before_page_visited: 'Before Page Visited',
|
||||
campaign_recipient_page_visited: 'Page Visited',
|
||||
campaign_recipient_after_page_visited: 'After Page Visited',
|
||||
campaign_recipient_deny_page_visited: 'Deny Page Visited'
|
||||
};
|
||||
|
||||
// create display options array with nice names
|
||||
const webhookEventDisplayOptions = webhookEventOptions.map((event) => ({
|
||||
value: event,
|
||||
label: webhookEventDisplayNames[event] || event
|
||||
}));
|
||||
|
||||
// map event names to bit positions (must match backend data.WebhookEventToBit)
|
||||
const webhookEventToBit = {
|
||||
campaign_closed: 1 << 0, // 1
|
||||
campaign_recipient_message_sent: 1 << 1, // 2
|
||||
campaign_recipient_message_failed: 1 << 2, // 4
|
||||
campaign_recipient_message_read: 1 << 3, // 8
|
||||
campaign_recipient_submitted_data: 1 << 4, // 16
|
||||
campaign_recipient_evasion_page_visited: 1 << 5, // 32
|
||||
campaign_recipient_before_page_visited: 1 << 6, // 64
|
||||
campaign_recipient_page_visited: 1 << 7, // 128
|
||||
campaign_recipient_after_page_visited: 1 << 8, // 256
|
||||
campaign_recipient_deny_page_visited: 1 << 9 // 512
|
||||
};
|
||||
|
||||
const WEBHOOK_EVENT_ALL_BITS = 1023; // 2^10 - 1, all 10 events selected
|
||||
|
||||
// convert array of event names to bitwise int
|
||||
const webhookEventsToBinary = (events) => {
|
||||
if (!events?.length) return 0; // 0 means all events
|
||||
return events.reduce((binary, event) => binary | (webhookEventToBit[event] || 0), 0);
|
||||
};
|
||||
|
||||
// convert bitwise int to array of event names (internal values, not display names)
|
||||
const webhookEventsFromBinary = (binary) => {
|
||||
if (binary === 0) return [...webhookEventOptions]; // 0 means all events, return all
|
||||
return webhookEventOptions.filter((event) => binary & webhookEventToBit[event]);
|
||||
};
|
||||
|
||||
// helper to get display name for an event
|
||||
const getEventDisplayName = (eventValue) => {
|
||||
return webhookEventDisplayNames[eventValue] || eventValue;
|
||||
};
|
||||
|
||||
let deleteValues = {
|
||||
id: null,
|
||||
name: null
|
||||
@@ -292,7 +378,8 @@
|
||||
obfuscate: false,
|
||||
selectedCount: 0,
|
||||
webhookValue: null,
|
||||
webhookIncludeData: false,
|
||||
webhookIncludeData: 'full',
|
||||
webhookEvents: [...webhookEventOptions], // all events selected by default
|
||||
jitterMin: 0,
|
||||
jitterMax: 0
|
||||
};
|
||||
@@ -305,7 +392,6 @@
|
||||
let isValidatingName = false;
|
||||
let weekDaysAvailable = [];
|
||||
let isDeleteAlertVisible = false;
|
||||
let webhookIncludeDataCheckbox = null;
|
||||
|
||||
$: {
|
||||
modalText = getModalText('campaign', modalMode);
|
||||
@@ -675,6 +761,7 @@
|
||||
constraintEndTime: contraintEndTimeUTC,
|
||||
webhookID: webhookMap.byValueOrNull(formValues.webhookValue),
|
||||
webhookIncludeData: formValues.webhookIncludeData,
|
||||
webhookEvents: webhookEventsToBinary(formValues.webhookEvents),
|
||||
jitterMin: formValues.jitterMin !== 0 ? formValues.jitterMin : null,
|
||||
jitterMax: formValues.jitterMax !== 0 ? formValues.jitterMax : null
|
||||
});
|
||||
@@ -741,6 +828,7 @@
|
||||
evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue),
|
||||
webhookID: webhookMap.byValueOrNull(formValues.webhookValue),
|
||||
webhookIncludeData: formValues.webhookIncludeData,
|
||||
webhookEvents: webhookEventsToBinary(formValues.webhookEvents),
|
||||
jitterMin: formValues.jitterMin !== 0 ? formValues.jitterMin : null,
|
||||
jitterMax: formValues.jitterMax !== 0 ? formValues.jitterMax : null
|
||||
});
|
||||
@@ -868,7 +956,8 @@
|
||||
obfuscate: false,
|
||||
selectedCount: 0,
|
||||
webhookValue: null,
|
||||
webhookIncludeData: false,
|
||||
webhookIncludeData: 'full',
|
||||
webhookEvents: [...webhookEventOptions], // all events selected by default
|
||||
jitterMin: 0,
|
||||
jitterMax: 0
|
||||
};
|
||||
@@ -976,7 +1065,8 @@
|
||||
obfuscate: campaign.obfuscate || false,
|
||||
template: templateMap.byKey(campaign.templateID),
|
||||
webhookValue: webhookMap.byKey(campaign.webhookID),
|
||||
webhookIncludeData: campaign.webhookIncludeData || false
|
||||
webhookIncludeData: campaign.webhookIncludeData || 'full',
|
||||
webhookEvents: webhookEventsFromBinary(campaign.webhookEvents || 0)
|
||||
};
|
||||
|
||||
if (copyMode) {
|
||||
@@ -1756,13 +1846,57 @@
|
||||
</div>
|
||||
{#if formValues.webhookValue}
|
||||
<div class="mb-6">
|
||||
<CheckboxField
|
||||
bind:bindTo={webhookIncludeDataCheckbox}
|
||||
<SelectSquare
|
||||
bind:value={formValues.webhookIncludeData}
|
||||
toolTipText="When enabled, captured data (credentials, cookies, etc.) will be included in the webhook payload"
|
||||
>
|
||||
Include captured data
|
||||
</CheckboxField>
|
||||
options={webhookDataLevelOptions}
|
||||
label="Webhook Data Level"
|
||||
toolTipText="Control what campaign and recipient information is sent to the webhook endpoint"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6 max-w-xl">
|
||||
<label class="flex flex-col">
|
||||
<div class="flex items-center py-2">
|
||||
<p class="font-semibold text-slate-600 dark:text-gray-400">Webhook Events</p>
|
||||
<div class="ml-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span class="text-xs"
|
||||
>({formValues.webhookEvents.length === webhookEventOptions.length
|
||||
? 'All'
|
||||
: formValues.webhookEvents.length} selected)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-2 max-h-48 overflow-y-auto">
|
||||
{#each webhookEventDisplayOptions as eventOption}
|
||||
{@const isSelected = formValues.webhookEvents.includes(eventOption.value)}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (isSelected) {
|
||||
// prevent unselecting the last item
|
||||
if (formValues.webhookEvents.length > 1) {
|
||||
formValues.webhookEvents = formValues.webhookEvents.filter(
|
||||
(e) => e !== eventOption.value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
formValues.webhookEvents = [
|
||||
...formValues.webhookEvents,
|
||||
eventOption.value
|
||||
];
|
||||
}
|
||||
}}
|
||||
class="flex flex-row items-center px-3 py-2 rounded-md text-xs transition-colors duration-200 flex-shrink-0 {isSelected
|
||||
? 'bg-green-50 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-2 border-green-400 dark:border-green-500 hover:bg-green-100 dark:hover:bg-green-900/40'
|
||||
: 'bg-white dark:bg-gray-900/60 text-gray-700 dark:text-gray-300 border-2 border-gray-200 dark:border-gray-700/60 hover:border-blue-300 dark:hover:border-highlight-blue/80 hover:bg-blue-50 dark:hover:bg-highlight-blue/20'}"
|
||||
style="max-width: 280px;"
|
||||
>
|
||||
<span class="truncate block" style="max-width: 250px;">
|
||||
{eventOption.label}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2171,10 +2305,16 @@
|
||||
<span class="text-grayblue-dark font-medium">Webhook:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">{formValues.webhookValue}</span
|
||||
>
|
||||
<span class="text-grayblue-dark font-medium">Include Data:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
>{formValues.webhookIncludeData ? 'Yes' : 'No'}</span
|
||||
<span class="text-grayblue-dark font-medium">Data Level:</span>
|
||||
<span class="text-pc-darkblue dark:text-white capitalize"
|
||||
>{formValues.webhookIncludeData}</span
|
||||
>
|
||||
<span class="text-grayblue-dark font-medium">Webhook Events:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{formValues.webhookEvents.length === webhookEventOptions.length
|
||||
? 'All Events'
|
||||
: formValues.webhookEvents.length + ' selected'}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if formValues.denyPageValue}
|
||||
|
||||
Reference in New Issue
Block a user