From 3b322e44e0de245805ce06b7ef429f295a960b71 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Fri, 29 May 2026 19:16:17 +0200 Subject: [PATCH] add enable PDF generation setting Signed-off-by: Ronni Skansing --- README.md | 1 + backend/app/controllers.go | 1 + backend/controller/reportTemplate.go | 7 ++ backend/data/option.go | 2 + backend/seed/migrate.go | 22 +++++ backend/service/option.go | 7 ++ frontend/src/lib/api/api.js | 4 +- .../src/routes/campaign/[id]/+page.svelte | 25 ++++- frontend/src/routes/settings/+page.svelte | 98 +++++++++++++++++++ 9 files changed, 164 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c47feb..b88f56b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Phishing Club providers a lot of features for simulation and red teaming, here a - **Multiple domains** - Auto TLS, custom sites and asset management - **Advanced delivery** - SMTP configs or custom API Sender with OAuth support - **Recipient tracking** - Groups, CSV import, repeat offender metrics +- **Campaign reports** - PDF export with a customizable HTML template - **Analytics** - Timelines, dashboards, per-user event history - **Automation** - HMAC-signed webhooks, REST API, import/export - **Multi-tenancy** - Segregated client handling and statistics for service providers diff --git a/backend/app/controllers.go b/backend/app/controllers.go index bf1b31f..9afbb07 100644 --- a/backend/app/controllers.go +++ b/backend/app/controllers.go @@ -213,6 +213,7 @@ func NewControllers( Common: common, ReportTemplateService: services.ReportTemplate, CampaignService: services.Campaign, + OptionService: services.Option, ExecPath: conf.RemoteBrowser.ExecPath, } diff --git a/backend/controller/reportTemplate.go b/backend/controller/reportTemplate.go index 6c9878e..e4d0b90 100644 --- a/backend/controller/reportTemplate.go +++ b/backend/controller/reportTemplate.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/phishingclub/phishingclub/data" "github.com/phishingclub/phishingclub/model" "github.com/phishingclub/phishingclub/remotebrowser" "github.com/phishingclub/phishingclub/repository" @@ -16,6 +17,7 @@ type ReportTemplate struct { Common ReportTemplateService *service.ReportTemplate CampaignService *service.Campaign + OptionService *service.Option ExecPath string } @@ -121,6 +123,11 @@ func (r *ReportTemplate) GeneratePDFByCampaignID(g *gin.Context) { if !ok { return } + opt, _ := r.OptionService.GetOptionWithoutAuth(g.Request.Context(), data.OptionKeyReportPDFEnabled) + if opt == nil || opt.Value.String() != "true" { + r.Response.NotFound(g) + return + } id, ok := r.handleParseIDParam(g) if !ok { return diff --git a/backend/data/option.go b/backend/data/option.go index f17fc32..0eb6028 100644 --- a/backend/data/option.go +++ b/backend/data/option.go @@ -37,6 +37,8 @@ const ( OptionKeyAutoPruneOrphanedRecipients = "auto_prune_orphaned_recipients" + OptionKeyReportPDFEnabled = "report_pdf_enabled" + OptionKeyObfuscationTemplate = "obfuscation_template" // OptionValueObfuscationTemplateDefault is the default HTML template for obfuscation // the template receives {{.Script}} variable containing the obfuscated javascript diff --git a/backend/seed/migrate.go b/backend/seed/migrate.go index 30930b8..d7d765d 100644 --- a/backend/seed/migrate.go +++ b/backend/seed/migrate.go @@ -426,6 +426,28 @@ func SeedSettings( } } } + { + // seed report PDF enabled (disabled by default) + id := uuid.New() + var c int64 + res := db. + Model(&database.Option{}). + Where("key = ?", data.OptionKeyReportPDFEnabled). + Count(&c) + if res.Error != nil { + return errs.Wrap(res.Error) + } + if c == 0 { + res = db.Create(&database.Option{ + ID: &id, + Key: data.OptionKeyReportPDFEnabled, + Value: "false", + }) + if res.Error != nil { + return errs.Wrap(res.Error) + } + } + } return nil } diff --git a/backend/service/option.go b/backend/service/option.go index 22c5ede..2e301b9 100644 --- a/backend/service/option.go +++ b/backend/service/option.go @@ -199,6 +199,13 @@ func (o *Option) SetOptionByKey( "value", ) } + case data.OptionKeyReportPDFEnabled: + if v != "true" && v != "false" { + return validate.WrapErrorWithField( + errs.NewValidationError(errors.New("invalid value")), + "value", + ) + } case data.OptionKeyObfuscationTemplate: // is allow listed default: diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 67dd0e0..e986d0b 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -2302,7 +2302,7 @@ export class API { /** * Get setting by key. * - * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key + * @param {'is_installed'|'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'|'report_pdf_enabled'} key * @returns {Promise} */ get: async (key) => { @@ -2333,7 +2333,7 @@ export class API { /** * Set setting by key and value. * - * @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'} key + * @param {'max_file_upload_size_mb'|'repeat_offender_months'|'sso_login'|'display_mode'|'obfuscation_template'|'report_pdf_enabled'} key * @param {string} value * @returns {Promise} */ diff --git a/frontend/src/routes/campaign/[id]/+page.svelte b/frontend/src/routes/campaign/[id]/+page.svelte index 1efcbf2..881b0c1 100644 --- a/frontend/src/routes/campaign/[id]/+page.svelte +++ b/frontend/src/routes/campaign/[id]/+page.svelte @@ -149,6 +149,7 @@ let isReportedCSVModalVisible = false; let isReportedCSVSubmitting = false; let isGenerateReportModalVisible = false; + let isReportPDFEnabled = false; let reportedCSVFile = null; let reportedCSVHeaders = []; let reportedCSVPreview = []; @@ -173,7 +174,7 @@ let lastPoll3399Nano = ''; // live remote browser sessions - /** @type {Map} */ + /** @type {Map} */ let liveSessions = new Map(); let liveSessionPollInterval = null; let streamModalVisible = false; @@ -191,6 +192,8 @@ } (async () => { + const pdfOpt = await api.option.get('report_pdf_enabled'); + isReportPDFEnabled = pdfOpt.success && pdfOpt.data?.value === 'true'; await refresh(); recipientTableUrlParams.onChange(refreshRecipients); eventsTableURLParams.onChange(refreshEvents); @@ -899,7 +902,13 @@ } }; + let isReportPDFDisabledModalVisible = false; + const onClickGenerateReport = () => { + if (!isReportPDFEnabled) { + isReportPDFDisabledModalVisible = true; + return; + } isGenerateReportModalVisible = true; }; @@ -2898,4 +2907,18 @@ automatically. + { + isReportPDFDisabledModalVisible = false; + return { success: true }; + }} + noCancel={true} + ok="OK" + > +
+ PDF report generation is not enabled.
Enable it in Settings under PDF Reports. +
+
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index d28db3b..ac0e982 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -12,6 +12,7 @@ import FormGrid from '$lib/components/FormGrid.svelte'; import Headline from '$lib/components/Headline.svelte'; import HeadTitle from '$lib/components/HeadTitle.svelte'; + import Alert from '$lib/components/Alert.svelte'; import Modal from '$lib/components/Modal.svelte'; import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte'; import PasswordField from '$lib/components/PasswordField.svelte'; @@ -99,6 +100,9 @@ let reportTemplateError = ''; let isReportTemplateSubmitting = false; let isWipingBrowserCache = false; + let isReportPDFEnabled = false; + let isReportPDFEnableModalVisible = false; + let isTogglingReportPDF = false; $: { isCompanyContext = appState.isCompanyContext(); @@ -123,6 +127,7 @@ await refreshBackupList(); await refreshDisplayMode(); await refreshAutoPrune(); + await refreshReportPDFEnabled(); if (!ssoSettingsFormValues.redirectURL) { ssoSettingsFormValues.redirectURL = `${location.origin}/api/v1/sso/entra-id/auth`; } @@ -605,6 +610,54 @@ reportTemplateError = ''; }; + const refreshReportPDFEnabled = async () => { + const response = await api.option.get('report_pdf_enabled'); + isReportPDFEnabled = response.success && response.data?.value === 'true'; + }; + + const onClickReportPDFToggle = () => { + if (isReportPDFEnabled) { + onDisableReportPDF(); + } else { + isReportPDFEnableModalVisible = true; + } + }; + + const onDisableReportPDF = async () => { + isTogglingReportPDF = true; + try { + const response = await api.option.set('report_pdf_enabled', 'false'); + if (response.success) { + isReportPDFEnabled = false; + addToast('PDF reports disabled', 'Success'); + } else { + addToast(response.error || 'Failed to update setting', 'Error'); + } + } catch (e) { + addToast('Failed to update setting', 'Error'); + } finally { + isTogglingReportPDF = false; + } + }; + + const onConfirmEnableReportPDF = async () => { + isTogglingReportPDF = true; + try { + const response = await api.option.set('report_pdf_enabled', 'true'); + if (response.success) { + isReportPDFEnabled = true; + isReportPDFEnableModalVisible = false; + addToast('PDF reports enabled', 'Success'); + } else { + addToast(response.error || 'Failed to update setting', 'Error'); + } + } catch (e) { + addToast('Failed to update setting', 'Error'); + } finally { + isTogglingReportPDF = false; + } + }; + const onWipeBrowserCache = async () => { isWipingBrowserCache = true; try { @@ -1062,6 +1115,37 @@ {/if} + +
+

+ PDF Reports +

+
+
+

+ Generate PDF reports for campaigns. Requires Chromium and system dependencies. +

+

+ {isReportPDFEnabled ? 'Enabled' : 'Disabled'} +

+
+
+ +
+
+
+
{/if} + {#if isReportPDFEnableModalVisible} + +
+

PDF report generation requires Chromium and additional system dependencies that are not part of the standard installation.

+

Before enabling, ensure the host has the required libraries and any AppArmor restrictions on unprivileged user namespaces have been addressed.

+

See the setup guide for dependency installation and AppArmor configuration.

+
+
+ {/if}