mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-03 02:55:54 +02:00
cleanup from old proxy page test
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -13,15 +13,12 @@ const (
|
||||
|
||||
// Page is a gorm data model
|
||||
type Page struct {
|
||||
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
|
||||
CreatedAt *time.Time `gorm:"not null;index;"`
|
||||
UpdatedAt *time.Time `gorm:"not null;index"`
|
||||
CompanyID *uuid.UUID `gorm:"index;uniqueIndex:idx_pages_unique_name_and_company_id;type:uuid"`
|
||||
Name string `gorm:"not null;index;uniqueIndex:idx_pages_unique_name_and_company_id;"`
|
||||
Content string `gorm:"not null;"`
|
||||
Type string `gorm:"not null;default:'regular';"`
|
||||
TargetURL string
|
||||
ProxyConfig string
|
||||
ID *uuid.UUID `gorm:"primary_key;not null;unique;type:uuid"`
|
||||
CreatedAt *time.Time `gorm:"not null;index;"`
|
||||
UpdatedAt *time.Time `gorm:"not null;index"`
|
||||
CompanyID *uuid.UUID `gorm:"index;uniqueIndex:idx_pages_unique_name_and_company_id;type:uuid"`
|
||||
Name string `gorm:"not null;index;uniqueIndex:idx_pages_unique_name_and_company_id;"`
|
||||
Content string `gorm:"not null;"`
|
||||
|
||||
// could has-one
|
||||
Company *Company
|
||||
|
||||
+8
-56
@@ -3,7 +3,6 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oapi-codegen/nullable"
|
||||
"github.com/phishingclub/phishingclub/validate"
|
||||
@@ -12,15 +11,12 @@ import (
|
||||
|
||||
// Page is a Page
|
||||
type Page struct {
|
||||
ID nullable.Nullable[uuid.UUID] `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
|
||||
Name nullable.Nullable[vo.String64] `json:"name"`
|
||||
Content nullable.Nullable[vo.OptionalString1MB] `json:"content"`
|
||||
Type nullable.Nullable[vo.String32] `json:"type"` // "regular" or "proxy"
|
||||
TargetURL nullable.Nullable[vo.OptionalString1024] `json:"targetURL"` // target url for proxy pages
|
||||
ProxyConfig nullable.Nullable[vo.OptionalString1MB] `json:"proxyConfig"` // yaml configuration for proxy
|
||||
ID nullable.Nullable[uuid.UUID] `json:"id"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
|
||||
Name nullable.Nullable[vo.String64] `json:"name"`
|
||||
Content nullable.Nullable[vo.OptionalString1MB] `json:"content"`
|
||||
|
||||
Company *Company `json:"-"`
|
||||
}
|
||||
@@ -31,34 +27,8 @@ func (p *Page) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// set default type if not specified
|
||||
if !p.Type.IsSpecified() {
|
||||
p.Type.Set(*vo.NewString32Must("regular"))
|
||||
}
|
||||
|
||||
pageType, err := p.Type.Get()
|
||||
if err != nil {
|
||||
return validate.WrapErrorWithField(errors.New("type is required"), "type")
|
||||
}
|
||||
|
||||
// validate type is either "regular" or "proxy"
|
||||
if pageType.String() != "regular" && pageType.String() != "proxy" {
|
||||
return validate.WrapErrorWithField(errors.New("type must be 'regular' or 'proxy'"), "type")
|
||||
}
|
||||
|
||||
if pageType.String() == "proxy" {
|
||||
// proxy pages require targetURL and proxyConfig
|
||||
if err := validate.NullableFieldRequired("targetURL", p.TargetURL); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate.NullableFieldRequired("proxyConfig", p.ProxyConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// regular pages require content
|
||||
if err := validate.NullableFieldRequired("content", p.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate.NullableFieldRequired("content", p.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -81,24 +51,6 @@ func (p *Page) ToDBMap() map[string]any {
|
||||
m["content"] = content.String()
|
||||
}
|
||||
}
|
||||
if p.Type.IsSpecified() {
|
||||
m["type"] = "regular"
|
||||
if pageType, err := p.Type.Get(); err == nil {
|
||||
m["type"] = pageType.String()
|
||||
}
|
||||
}
|
||||
if p.TargetURL.IsSpecified() {
|
||||
m["target_url"] = nil
|
||||
if targetURL, err := p.TargetURL.Get(); err == nil {
|
||||
m["target_url"] = targetURL.String()
|
||||
}
|
||||
}
|
||||
if p.ProxyConfig.IsSpecified() {
|
||||
m["proxy_config"] = nil
|
||||
if proxyConfig, err := p.ProxyConfig.Get(); err == nil {
|
||||
m["proxy_config"] = proxyConfig.String()
|
||||
}
|
||||
}
|
||||
if p.CompanyID.IsSpecified() {
|
||||
if p.CompanyID.IsNull() {
|
||||
m["company_id"] = nil
|
||||
|
||||
@@ -254,34 +254,12 @@ func ToPage(row *database.Page) (*model.Page, error) {
|
||||
}
|
||||
content := nullable.NewNullableWithValue(*c)
|
||||
|
||||
// Handle proxy fields
|
||||
typeValue := row.Type
|
||||
if typeValue == "" {
|
||||
typeValue = "regular"
|
||||
}
|
||||
pageType := nullable.NewNullableWithValue(*vo.NewString32Must(typeValue))
|
||||
|
||||
targetURL, err := vo.NewOptionalString1024(row.TargetURL)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
targetURLNullable := nullable.NewNullableWithValue(*targetURL)
|
||||
|
||||
proxyConfig, err := vo.NewOptionalString1MB(row.ProxyConfig)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
proxyConfigNullable := nullable.NewNullableWithValue(*proxyConfig)
|
||||
|
||||
return &model.Page{
|
||||
ID: id,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CompanyID: companyID,
|
||||
Name: name,
|
||||
Content: content,
|
||||
Type: pageType,
|
||||
TargetURL: targetURLNullable,
|
||||
ProxyConfig: proxyConfigNullable,
|
||||
ID: id,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
CompanyID: companyID,
|
||||
Name: name,
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+10
-190
@@ -2,12 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/phishingclub/phishingclub/data"
|
||||
@@ -15,7 +11,6 @@ import (
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/validate"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -29,39 +24,6 @@ type Page struct {
|
||||
DomainRepository *repository.Domain
|
||||
}
|
||||
|
||||
// ProxyConfig represents the YAML configuration for proxy pages
|
||||
type ProxyConfig struct {
|
||||
Default map[string]interface{} `yaml:"default,omitempty"`
|
||||
Hosts map[string]ProxyHostConfig `yaml:",inline"`
|
||||
}
|
||||
|
||||
// ProxyHostConfig represents configuration for a specific host
|
||||
type ProxyHostConfig struct {
|
||||
Proxy string `yaml:"proxy,omitempty"`
|
||||
Domain string `yaml:"domain,omitempty"`
|
||||
Capture []ProxyCaptureRule `yaml:"capture,omitempty"`
|
||||
Replace []ProxyReplaceRule `yaml:"replace,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyCaptureRule represents a capture rule
|
||||
type ProxyCaptureRule struct {
|
||||
Name string `yaml:"name"`
|
||||
Method string `yaml:"method,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Pattern string `yaml:"pattern,omitempty"`
|
||||
Find string `yaml:"find"`
|
||||
From string `yaml:"from,omitempty"`
|
||||
Required *bool `yaml:"required,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyReplaceRule represents a replace rule
|
||||
type ProxyReplaceRule struct {
|
||||
Name string `yaml:"name"`
|
||||
Find string `yaml:"find"`
|
||||
Replace string `yaml:"replace"`
|
||||
From string `yaml:"from,omitempty"`
|
||||
}
|
||||
|
||||
// Create creates a new page
|
||||
func (p *Page) Create(
|
||||
ctx context.Context,
|
||||
@@ -89,20 +51,11 @@ func (p *Page) Create(
|
||||
p.Logger.Errorw("failed to validate page", "error", err)
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
// validate based on page type
|
||||
pageType, _ := page.Type.Get()
|
||||
if pageType.String() == "proxy" {
|
||||
// validate proxy configuration
|
||||
if err := p.validateProxyPage(ctx, page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// validate template content for regular pages
|
||||
if content, err := page.Content.Get(); err == nil {
|
||||
if err := p.TemplateService.ValidatePageTemplate(content.String()); err != nil {
|
||||
p.Logger.Errorw("failed to validate page template", "error", err)
|
||||
return nil, validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content")
|
||||
}
|
||||
// validate template content
|
||||
if content, err := page.Content.Get(); err == nil {
|
||||
if err := p.TemplateService.ValidatePageTemplate(content.String()); err != nil {
|
||||
p.Logger.Errorw("failed to validate page template", "error", err)
|
||||
return nil, validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content")
|
||||
}
|
||||
}
|
||||
// check uniqueness
|
||||
@@ -308,33 +261,15 @@ func (p *Page) UpdateByID(
|
||||
}
|
||||
current.Name.Set(v)
|
||||
}
|
||||
if v, err := page.Type.Get(); err == nil {
|
||||
current.Type.Set(v)
|
||||
}
|
||||
if v, err := page.TargetURL.Get(); err == nil {
|
||||
current.TargetURL.Set(v)
|
||||
}
|
||||
if v, err := page.ProxyConfig.Get(); err == nil {
|
||||
current.ProxyConfig.Set(v)
|
||||
}
|
||||
if v, err := page.Content.Get(); err == nil {
|
||||
current.Content.Set(v)
|
||||
}
|
||||
|
||||
// validate based on updated page type
|
||||
updatedPageType, _ := current.Type.Get()
|
||||
if updatedPageType.String() == "proxy" {
|
||||
// validate proxy configuration
|
||||
if err := p.validateProxyPage(ctx, current); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// validate template content for regular pages
|
||||
if content, err := current.Content.Get(); err == nil {
|
||||
if err := p.TemplateService.ValidatePageTemplate(content.String()); err != nil {
|
||||
p.Logger.Errorw("failed to validate page template", "error", err)
|
||||
return validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content")
|
||||
}
|
||||
// validate template content
|
||||
if content, err := current.Content.Get(); err == nil {
|
||||
if err := p.TemplateService.ValidatePageTemplate(content.String()); err != nil {
|
||||
p.Logger.Errorw("failed to validate page template", "error", err)
|
||||
return validate.WrapErrorWithField(errors.New("invalid template: "+err.Error()), "content")
|
||||
}
|
||||
}
|
||||
// update page
|
||||
@@ -353,121 +288,6 @@ func (p *Page) UpdateByID(
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateProxyPage validates proxy page configuration
|
||||
func (p *Page) validateProxyPage(ctx context.Context, page *model.Page) error {
|
||||
// validate target URL format
|
||||
targetURL, err := page.TargetURL.Get()
|
||||
if err != nil {
|
||||
return validate.WrapErrorWithField(errors.New("target URL is required for proxy pages"), "targetURL")
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(targetURL.String())
|
||||
if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
|
||||
return validate.WrapErrorWithField(errors.New("invalid target URL format - must be a valid HTTP or HTTPS URL"), "targetURL")
|
||||
}
|
||||
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return validate.WrapErrorWithField(errors.New("target URL must use HTTP or HTTPS protocol"), "targetURL")
|
||||
}
|
||||
|
||||
// validate proxy configuration YAML
|
||||
proxyConfig, err := page.ProxyConfig.Get()
|
||||
if err != nil {
|
||||
return validate.WrapErrorWithField(errors.New("proxy configuration is required for proxy pages"), "proxyConfig")
|
||||
}
|
||||
|
||||
var config ProxyConfig
|
||||
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
||||
return validate.WrapErrorWithField(errors.New("invalid YAML format: "+err.Error()), "proxyConfig")
|
||||
}
|
||||
|
||||
// validate that all referenced domains in the config support proxy
|
||||
for hostname, hostConfig := range config.Hosts {
|
||||
if hostConfig.Domain != "" {
|
||||
domainName, err := vo.NewString255(hostConfig.Domain)
|
||||
if err != nil {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("invalid domain name format"),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
|
||||
_, err = p.DomainRepository.GetByName(ctx, domainName, &repository.DomainOption{})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("referenced domain '"+hostConfig.Domain+"' not found"),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// validate capture rules
|
||||
for _, capture := range hostConfig.Capture {
|
||||
if capture.Name == "" {
|
||||
return validate.WrapErrorWithField(errors.New("capture rule name is required"), "proxyConfig")
|
||||
}
|
||||
if capture.Pattern == "" && capture.Path == "" {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("capture rule must have either pattern or path"),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
if capture.Pattern != "" {
|
||||
if _, err := regexp.Compile(capture.Pattern); err != nil {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("invalid regex pattern in capture rule: "+err.Error()),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
}
|
||||
if capture.Path != "" {
|
||||
if _, err := regexp.Compile(capture.Path); err != nil {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("invalid regex pattern for path in capture rule: "+err.Error()),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
}
|
||||
if capture.From != "" {
|
||||
validFromValues := []string{"request_body", "request_header", "response_body", "response_header", "any"}
|
||||
valid := false
|
||||
for _, validFrom := range validFromValues {
|
||||
if capture.From == validFrom {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("invalid 'from' value in capture rule, must be one of: "+strings.Join(validFromValues, ", ")),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate replace rules
|
||||
for _, replace := range hostConfig.Replace {
|
||||
if replace.Find == "" {
|
||||
return validate.WrapErrorWithField(errors.New("replace rule 'find' is required"), "proxyConfig")
|
||||
}
|
||||
if _, err := regexp.Compile(replace.Find); err != nil {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New("invalid regex pattern in replace rule 'find': "+err.Error()),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
p.Logger.Debugw("validated proxy host config", "hostname", hostname)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByID deletes a page by ID
|
||||
func (p *Page) DeleteByID(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -1314,15 +1314,13 @@ export class API {
|
||||
* @param {string} name
|
||||
* @param {string} content
|
||||
* @param {string} companyID
|
||||
* @param {object} additionalFields - Optional additional fields for Proxy pages
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
create: async (name, content, companyID, additionalFields = {}) => {
|
||||
create: async (name, content, companyID) => {
|
||||
const payload = {
|
||||
name: name,
|
||||
content: content,
|
||||
companyID: companyID,
|
||||
...additionalFields
|
||||
companyID: companyID
|
||||
};
|
||||
return await postJSON(this.getPath('/page'), payload);
|
||||
},
|
||||
|
||||
@@ -388,11 +388,19 @@
|
||||
isDetailsVisible = true;
|
||||
}}
|
||||
type="button"
|
||||
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md w-36 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 mb-2 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 transition-colors duration-200"
|
||||
class="h-8 border-2 rounded-md w-36 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 mb-2 transition-colors duration-200"
|
||||
class:font-bold={isDetailsVisible}
|
||||
class:bg-cta-blue={isDetailsVisible}
|
||||
class:dark:bg-indigo-600={isDetailsVisible}
|
||||
class:bg-blue-600={isDetailsVisible}
|
||||
class:dark:bg-blue-500={isDetailsVisible}
|
||||
class:text-white={isDetailsVisible}
|
||||
class:border-blue-600={isDetailsVisible}
|
||||
class:dark:border-blue-500={isDetailsVisible}
|
||||
class:text-gray-700={!isDetailsVisible}
|
||||
class:dark:text-gray-200={!isDetailsVisible}
|
||||
class:bg-white={!isDetailsVisible}
|
||||
class:dark:bg-gray-700={!isDetailsVisible}
|
||||
class:border-gray-300={!isDetailsVisible}
|
||||
class:dark:border-gray-600={!isDetailsVisible}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -414,11 +422,19 @@
|
||||
isDetailsVisible = false;
|
||||
}}
|
||||
type="button"
|
||||
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md w-36 text-center cursor-pointer hover:opacity-80 ml-1 flex items-center justify-center gap-2 text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 transition-colors duration-200"
|
||||
class="h-8 border-2 rounded-md w-36 text-center cursor-pointer hover:opacity-80 ml-1 flex items-center justify-center gap-2 transition-colors duration-200"
|
||||
class:font-bold={!isDetailsVisible}
|
||||
class:bg-cta-blue={!isDetailsVisible}
|
||||
class:dark:bg-indigo-600={!isDetailsVisible}
|
||||
class:bg-blue-600={!isDetailsVisible}
|
||||
class:dark:bg-blue-500={!isDetailsVisible}
|
||||
class:text-white={!isDetailsVisible}
|
||||
class:border-blue-600={!isDetailsVisible}
|
||||
class:dark:border-blue-500={!isDetailsVisible}
|
||||
class:text-gray-700={isDetailsVisible}
|
||||
class:dark:text-gray-200={isDetailsVisible}
|
||||
class:bg-white={isDetailsVisible}
|
||||
class:dark:bg-gray-700={isDetailsVisible}
|
||||
class:border-gray-300={isDetailsVisible}
|
||||
class:dark:border-gray-600={isDetailsVisible}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import TableCellAction from '$lib/components/table/TableCellAction.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import FormGrid from '$lib/components/FormGrid.svelte';
|
||||
import ProxySvgIcon from '$lib/components/ProxySvgIcon.svelte';
|
||||
|
||||
import BigButton from '$lib/components/BigButton.svelte';
|
||||
import FormColumns from '$lib/components/FormColumns.svelte';
|
||||
import FormColumn from '$lib/components/FormColumn.svelte';
|
||||
@@ -34,7 +34,6 @@
|
||||
import { fetchAllRows } from '$lib/utils/api-utils';
|
||||
import { BiMap } from '$lib/utils/maps';
|
||||
import AutoRefresh from '$lib/components/AutoRefresh.svelte';
|
||||
import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -44,10 +43,7 @@
|
||||
let formValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
content: null,
|
||||
type: 'regular',
|
||||
targetURL: null,
|
||||
proxyConfig: null
|
||||
content: null
|
||||
};
|
||||
let isSubmitting = false;
|
||||
|
||||
@@ -68,54 +64,6 @@
|
||||
name: null
|
||||
};
|
||||
|
||||
// proxy example configuration - simplified to only capture and replacement rules
|
||||
const proxyExample = `capture:
|
||||
- name: 'login credentials'
|
||||
method: 'POST' # optional, default GET
|
||||
path: '/login' # regex path pattern - matches /login exactly
|
||||
find: 'username=([^&]+)&password=([^&]+)' # REQUIRED - regex pattern to capture data
|
||||
from: 'request_body' # where to capture from: request_body, request_header, response_body, response_header, any
|
||||
# required: true # default - all captures are required unless explicitly set to false
|
||||
|
||||
- name: 'has completed login'
|
||||
method: 'GET'
|
||||
path: '/secure' # navigation tracking - just checks if user visited this path
|
||||
# no find pattern needed for path-based navigation tracking
|
||||
# required: true # default - user must visit /secure before campaign progresses
|
||||
|
||||
- name: 'form submission'
|
||||
method: 'POST'
|
||||
path: '/submit-data' # tracks POST requests to this endpoint
|
||||
# no find pattern needed - just tracking that the form was submitted
|
||||
|
||||
- name: 'profile update'
|
||||
method: 'PUT'
|
||||
path: '/api/profile' # tracks PUT requests for profile updates
|
||||
# navigation tracking works with any HTTP method
|
||||
|
||||
- name: 'api tokens'
|
||||
path: '/api/v\\d+/auth.*' # regex - matches /api/v1/auth, /api/v2/auth/token, etc.
|
||||
find: 'token=([a-zA-Z0-9]+)' # REQUIRED - all captures must have a find pattern
|
||||
from: 'response_body'
|
||||
|
||||
- name: 'optional tracking data'
|
||||
path: '^/dashboard' # regex - matches paths starting with /dashboard
|
||||
find: 'session_id=([a-f0-9]+)' # REQUIRED - find pattern is mandatory
|
||||
from: 'response_header'
|
||||
required: false # explicitly mark as optional - campaign will progress without this
|
||||
|
||||
replace:
|
||||
- name: 'replace logo'
|
||||
find: 'https://target\\.example\\.com/logo\\.png'
|
||||
replace: 'https://evil.domain.com/assets/logo.png'
|
||||
|
||||
- name: 'replace links'
|
||||
find: 'href="([^"]*target\\.example\\.com[^"]*)"'
|
||||
replace: 'href="https://evil.domain.com$1"'`;
|
||||
|
||||
$: isRegularPage = formValues.type === 'regular';
|
||||
$: isProxyPage = formValues.type === 'proxy';
|
||||
|
||||
$: {
|
||||
modalText = getModalText('page', modalMode);
|
||||
}
|
||||
@@ -209,20 +157,7 @@ replace:
|
||||
|
||||
const create = async () => {
|
||||
try {
|
||||
const pageData = {
|
||||
name: formValues.name,
|
||||
type: formValues.type,
|
||||
content: isRegularPage ? formValues.content : null,
|
||||
targetURL: isProxyPage ? formValues.targetURL : null,
|
||||
proxyConfig: isProxyPage ? formValues.proxyConfig : null
|
||||
};
|
||||
|
||||
const res = await api.page.create(
|
||||
pageData.name,
|
||||
pageData.content,
|
||||
contextCompanyID,
|
||||
pageData
|
||||
);
|
||||
const res = await api.page.create(formValues.name, formValues.content, contextCompanyID);
|
||||
if (!res.success) {
|
||||
formError = res.error;
|
||||
return;
|
||||
@@ -240,10 +175,7 @@ replace:
|
||||
try {
|
||||
const updateData = {
|
||||
name: formValues.name,
|
||||
type: formValues.type,
|
||||
content: isRegularPage ? formValues.content : null,
|
||||
targetURL: isProxyPage ? formValues.targetURL : null,
|
||||
proxyConfig: isProxyPage ? formValues.proxyConfig : null
|
||||
content: formValues.content
|
||||
};
|
||||
|
||||
const res = await api.page.update(formValues.id, updateData);
|
||||
@@ -288,9 +220,6 @@ replace:
|
||||
formValues.content = '';
|
||||
formValues.name = '';
|
||||
formValues.id = '';
|
||||
formValues.type = 'regular';
|
||||
formValues.targetURL = '';
|
||||
formValues.proxyConfig = '';
|
||||
form.reset();
|
||||
formError = '';
|
||||
};
|
||||
@@ -305,10 +234,7 @@ replace:
|
||||
formValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
content: null,
|
||||
type: 'regular',
|
||||
targetURL: null,
|
||||
proxyConfig: null
|
||||
content: null
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -337,10 +263,7 @@ replace:
|
||||
formValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
content: null,
|
||||
type: 'regular',
|
||||
targetURL: null,
|
||||
proxyConfig: null
|
||||
content: null
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -366,9 +289,6 @@ replace:
|
||||
formValues.id = page.id;
|
||||
formValues.name = page.name;
|
||||
formValues.content = page.content || '';
|
||||
formValues.type = page.type && page.type.trim() !== '' ? page.type : 'regular';
|
||||
formValues.targetURL = page.targetURL || '';
|
||||
formValues.proxyConfig = page.proxyConfig || '';
|
||||
};
|
||||
|
||||
/** @param {*} event */
|
||||
@@ -440,115 +360,18 @@ replace:
|
||||
</Table>
|
||||
<Modal headerText={modalText} visible={isModalVisible} onClose={closeModal} {isSubmitting}>
|
||||
<FormGrid on:submit={onSubmit} bind:bindTo={form} {isSubmitting}>
|
||||
<div class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-8">
|
||||
<!-- Basic Information Section -->
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
|
||||
Basic Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={64}
|
||||
bind:value={formValues.name}
|
||||
placeholder="Intranet login">Name</TextField
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col py-2">
|
||||
<div class="flex items-center">
|
||||
<p
|
||||
class="font-semibold text-slate-600 dark:text-gray-300 py-2 transition-colors duration-200"
|
||||
>
|
||||
Type
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<label
|
||||
class="flex items-center space-x-2 px-3 py-2 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||
class:bg-blue-50={formValues.type === 'regular'}
|
||||
class:border-blue-300={formValues.type === 'regular'}
|
||||
class:dark:bg-blue-900={formValues.type === 'regular'}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={formValues.type}
|
||||
value="regular"
|
||||
class="text-blue-600"
|
||||
/>
|
||||
<span class="text-sm text-slate-600 dark:text-gray-300">📄 Regular</span>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-center space-x-2 px-3 py-2 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||
class:bg-blue-50={formValues.type === 'proxy'}
|
||||
class:border-blue-300={formValues.type === 'proxy'}
|
||||
class:dark:bg-blue-900={formValues.type === 'proxy'}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={formValues.type}
|
||||
value="proxy"
|
||||
class="text-blue-600"
|
||||
/>
|
||||
<span
|
||||
class="text-sm text-slate-600 dark:text-gray-300 flex items-center gap-1"
|
||||
>
|
||||
<ProxySvgIcon size="w-4 h-4" />
|
||||
Proxy
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Editor contentType="page" {domainMap} bind:value={formValues.content}>
|
||||
<div class="pl-4">
|
||||
<TextField
|
||||
minLength={1}
|
||||
maxLength={64}
|
||||
required
|
||||
bind:value={formValues.name}
|
||||
placeholder="Intranet login">Name</TextField
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Content Configuration Section -->
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
|
||||
{#if isProxyPage}
|
||||
Proxy Configuration
|
||||
{:else}
|
||||
Page Content
|
||||
{/if}
|
||||
</h3>
|
||||
|
||||
{#if isRegularPage}
|
||||
<Editor contentType="page" {domainMap} bind:value={formValues.content} />
|
||||
{/if}
|
||||
|
||||
{#if isProxyPage}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col py-2 w-full">
|
||||
<div class="flex items-center">
|
||||
<p class="font-bold text-slate-600 dark:text-gray-300 py-2">
|
||||
Proxy Capture & Replacement Rules (YAML)
|
||||
</p>
|
||||
<div class="ml-2 text-xs text-gray-500">
|
||||
Data captures require a 'find' pattern. Path-based navigation tracking (any
|
||||
method) doesn't need 'find'. All captures are required by default.
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-80vw">
|
||||
<SimpleCodeEditor
|
||||
bind:value={formValues.proxyConfig}
|
||||
height="medium"
|
||||
language="yaml"
|
||||
placeholder={proxyExample}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<FormError message={formError} />
|
||||
</div>
|
||||
|
||||
</Editor>
|
||||
<FormError message={formError} />
|
||||
<FormFooter {closeModal} {isSubmitting} />
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user