add enable PDF generation setting

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-05-29 19:16:17 +02:00
parent 23539c10c9
commit 3b322e44e0
9 changed files with 164 additions and 3 deletions
+1
View File
@@ -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
+1
View File
@@ -213,6 +213,7 @@ func NewControllers(
Common: common,
ReportTemplateService: services.ReportTemplate,
CampaignService: services.Campaign,
OptionService: services.Option,
ExecPath: conf.RemoteBrowser.ExecPath,
}
+7
View File
@@ -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
+2
View File
@@ -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
+22
View File
@@ -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
}
+7
View File
@@ -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:
+2 -2
View File
@@ -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<ApiResponse>}
*/
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<ApiResponse>}
*/
+24 -1
View File
@@ -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<string, {crID: string, campaignID: string, recipientID: string, createdAt: string, victimConnected: boolean}>} */
/** @type {Map<string, {crID: string, campaignID: string, recipientID: string, createdAt: string, victimConnected: boolean, canStream: boolean}>} */
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.
</div>
</Alert>
<Alert
headline="PDF reports disabled"
bind:visible={isReportPDFDisabledModalVisible}
onConfirm={() => {
isReportPDFDisabledModalVisible = false;
return { success: true };
}}
noCancel={true}
ok="OK"
>
<div class="mt-4 text-gray-700 dark:text-gray-200">
PDF report generation is not enabled. <br />Enable it in <strong>Settings</strong> under PDF Reports.
</div>
</Alert>
</main>
+98
View File
@@ -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 @@
</div>
{/if}
<!-- PDF Reports Section -->
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 h-[420px] flex flex-col transition-colors duration-200"
>
<h2
class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-6 transition-colors duration-200"
>
PDF Reports
</h2>
<div class="flex flex-col h-full">
<div class="space-y-4">
<p class="text-gray-600 dark:text-gray-300 text-sm transition-colors duration-200">
Generate PDF reports for campaigns. Requires Chromium and system dependencies.
</p>
<p class="text-sm font-medium transition-colors duration-200" class:text-green-600={isReportPDFEnabled} class:dark:text-green-400={isReportPDFEnabled} class:text-gray-500={!isReportPDFEnabled} class:dark:text-gray-400={!isReportPDFEnabled}>
{isReportPDFEnabled ? 'Enabled' : 'Disabled'}
</p>
</div>
<div class="mt-auto pt-4">
<Button
size={'large'}
backgroundColor={isReportPDFEnabled ? 'bg-red-600' : 'bg-cta-blue'}
disabled={isTogglingReportPDF}
on:click={onClickReportPDFToggle}
>
{isReportPDFEnabled ? 'Disable' : 'Enable'}
</Button>
</div>
</div>
</div>
<!-- Browser Cache Section -->
<div
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm dark:shadow-gray-900/50 border border-gray-100 dark:border-gray-700 h-[420px] flex flex-col transition-colors duration-200"
@@ -1552,4 +1636,18 @@
</FormGrid>
</Modal>
{/if}
{#if isReportPDFEnableModalVisible}
<Alert
headline="Enable PDF Reports"
bind:visible={isReportPDFEnableModalVisible}
onConfirm={onConfirmEnableReportPDF}
ok="Enable"
>
<div class="mt-4 text-gray-700 dark:text-gray-200 space-y-3">
<p>PDF report generation requires Chromium and additional system dependencies that are not part of the standard installation.</p>
<p>Before enabling, ensure the host has the required libraries and any AppArmor restrictions on unprivileged user namespaces have been addressed.</p>
<p>See <a href="https://phishing.club/guide/settings/#pdf-reports" target="_blank" class="underline">the setup guide</a> for dependency installation and AppArmor configuration.</p>
</div>
</Alert>
{/if}
</main>