From 74ff6838cc216cd9d374e669c331fbc0eccd0d83 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Tue, 8 Apr 2025 09:26:18 +0200 Subject: [PATCH 01/10] populate Admin Rights by Ownership to permission object --- src/helpers/jwtHelper.ts | 5 ++- src/helpers/permissionHelper.ts | 62 ++++++++++++++++++++++++++------- src/type/permissionTypes.ts | 12 +++++++ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/helpers/jwtHelper.ts b/src/helpers/jwtHelper.ts index c16f2a2..5708ab8 100644 --- a/src/helpers/jwtHelper.ts +++ b/src/helpers/jwtHelper.ts @@ -58,7 +58,10 @@ export abstract class JWTHelper { let rolePermissions = userRoles.length != 0 ? await RolePermissionService.getByRoles(userRoles.map((e) => e.id)) : []; let rolePermissionStrings = rolePermissions.map((e) => e.permission); - let permissionObject = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]); + let permissionObject = PermissionHelper.convertToObject( + [...userPermissionStrings, ...rolePermissionStrings], + isOwner + ); let jwtData: JWTToken = { userId: id, diff --git a/src/helpers/permissionHelper.ts b/src/helpers/permissionHelper.ts index 86b27e2..6a83f92 100644 --- a/src/helpers/permissionHelper.ts +++ b/src/helpers/permissionHelper.ts @@ -4,9 +4,11 @@ import { permissionModules, PermissionObject, PermissionSection, + permissionSections, PermissionString, PermissionType, permissionTypes, + sectionsAndModules, } from "../type/permissionTypes"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; @@ -17,8 +19,8 @@ export default class PermissionHelper { section: PermissionSection, module?: PermissionModule ) { - if (type == "admin") return permissions?.admin ?? false; - if (permissions?.admin) return true; + if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; + if (permissions?.admin || permissions?.adminByOwner) return true; if ( (!module && permissions[section] != undefined && @@ -50,8 +52,8 @@ export default class PermissionHelper { type: PermissionType | "admin", section: PermissionSection ): boolean { - if (type == "admin") return permissions?.admin ?? false; - if (permissions?.admin) return true; + if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; + if (permissions?.admin || permissions?.adminByOwner) return true; if ( permissions[section]?.all == "*" || permissions[section]?.all?.includes(type) || @@ -73,6 +75,11 @@ export default class PermissionHelper { }, false); } + static canValue(permissions: PermissionObject, key: string, emptyIfAdmin: boolean = false): string { + if (emptyIfAdmin && (permissions.admin || permissions.adminByOwner)) return ""; + return permissions?.additional?.[key] ?? ""; + } + static passCheckMiddleware( requiredPermissions: PermissionType | "admin", section: PermissionSection, @@ -159,14 +166,28 @@ export default class PermissionHelper { }; } - static convertToObject(permissions: Array): PermissionObject { - if (permissions.includes("*")) { + static convertToObject(permissions: Array, isOwner: boolean = false): PermissionObject { + let isAdmin = permissions.includes("*"); + + let additional: { [key: string]: string } = {}; + let additionalPermissions = permissions.map((e) => e.split(".")).filter((e) => e[0] == "additional") as Array< + ["additional", string, string] + >; + for (let split of additionalPermissions) { + let module = sectionsAndModules.additional.find((a) => a.key == split[1]); + if (!isAdmin || (isAdmin && !module.emptyIfAdmin)) additional[split[1]] = split[2]; + } + + if (isAdmin) { return { admin: true, + adminByOwner: isOwner, + ...(Object.keys(additional).length > 0 && { additional }), }; } + let output: PermissionObject = {}; - let splitPermissions = permissions.map((e) => e.split(".")) as Array< + let splitPermissions = permissions.map((e) => e.split(".")).filter((e) => e[0] != "additional") as Array< [PermissionSection, PermissionModule | PermissionType | "*", PermissionType | "*"] >; for (let split of splitPermissions) { @@ -208,15 +229,31 @@ export default class PermissionHelper { } } } - return output; + + return { + adminByOwner: isOwner, + ...output, + ...(Object.keys(additional).length > 0 && { additional }), + }; } static convertToStringArray(permissions: PermissionObject): Array { - if (permissions?.admin) { - return ["*"]; + let isAdmin = permissions?.admin; + + let additional: Array = []; + let additionalPermissions = Object.entries(permissions?.additional ?? {}); + for (let add of additionalPermissions) { + additional.push(`additional.${add[0]}.${add[1]}`); } + + if (isAdmin) { + return ["*", ...additional]; + } + let output: Array = []; - let sections = Object.keys(permissions) as Array; + let sections = Object.keys(permissions).filter((m: PermissionSection) => + permissionSections.includes(m) + ) as Array; for (let section of sections) { if (permissions[section].all) { let types = permissions[section].all; @@ -242,7 +279,8 @@ export default class PermissionHelper { } } } - return output; + + return [...output, ...additional]; } static getWhatToAdd(before: Array, after: Array): Array { diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index e9ed138..1061d49 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -30,6 +30,7 @@ export type PermissionString = | `${PermissionSection}.${PermissionModule}.*` // für alle Berechtigungen in einem Modul | `${PermissionSection}.${PermissionType}` // für spezifische Berechtigungen in einem Abschnitt | `${PermissionSection}.*` // für alle Berechtigungen in einem Abschnitt + | `additional.${string}.${string}` // additional | "*"; // für Admin export type PermissionObject = { @@ -38,10 +39,20 @@ export type PermissionObject = { } & { all?: Array | "*" }; } & { admin?: boolean; + adminByOwner?: boolean; +} & { + additional?: { [key: string]: string }; }; export type SectionsAndModulesObject = { [section in PermissionSection]: Array; +} & { + additional?: Array<{ + key: string; + name: string; + type: "number" | "string"; + emptyIfAdmin: boolean; + }>; }; export const permissionSections: Array = ["club", "configuration", "management"]; @@ -85,4 +96,5 @@ export const sectionsAndModules: SectionsAndModulesObject = { "newsletter_config", ], management: ["user", "role", "webapi", "backup"], + additional: [], }; -- 2.45.3 From a05e7fcf638dd6fff64b0ac2f53c63eede8b8f01 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 9 Apr 2025 20:07:56 +0200 Subject: [PATCH 02/10] fix: prevent print all by empty ids array --- src/helpers/newsletterHelper.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 5726dbd..267f282 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -140,9 +140,12 @@ export abstract class NewsletterHelper { } } - let members = await MemberService.getAll({ noLimit: true, ids: queryMemberIds }); - - return members[0]; + if (queryMemberIds.length == 0) { + return []; + } else { + let members = await MemberService.getAll({ noLimit: true, ids: queryMemberIds }); + return members[0]; + } } public static getICSFilePath(newsletter: newsletter) { -- 2.45.3 From 43f397f0e2487e22d6bce6735b4ccd61358b17af Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 9 Apr 2025 20:08:19 +0200 Subject: [PATCH 03/10] fix: prevent whitespace in printout filename --- src/helpers/newsletterHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 267f282..8ab1b05 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -268,7 +268,7 @@ export abstract class NewsletterHelper { await PdfExport.renderFile({ template: "newsletter", title: `Newsletter von ${CLUB_NAME}`, - filename: `${rec.lastname}_${rec.firstname}_${rec.id}`, + filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replace(" ", "-"), folder: `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, data: data, }) -- 2.45.3 From 8c333b1b89b70069073a0494d3da62ed3c7cb8c7 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 08:02:32 +0200 Subject: [PATCH 04/10] enhance: provide count of receivers at newsletter --- .../admin/club/newsletterController.ts | 28 ++++++ src/helpers/newsletterHelper.ts | 94 +++++++++++-------- src/routes/admin/club/newsletter.ts | 10 ++ 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/controller/admin/club/newsletterController.ts b/src/controller/admin/club/newsletterController.ts index 294b6e3..9338732 100644 --- a/src/controller/admin/club/newsletterController.ts +++ b/src/controller/admin/club/newsletterController.ts @@ -164,6 +164,34 @@ export async function createNewsletterPrintoutPreviewById(req: Request, res: Res res.send(pdfbuffer); } +/** + * @description get all members receiving a newsletter printout by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterPrintReceiversById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + let recipients = await NewsletterHelper.getPrintRecipients(newsletterId); + + res.json(recipients); +} + +/** + * @description get all members receiving a newsletter mail by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getNewsletterMailReceiversById(req: Request, res: Response): Promise { + let newsletterId = parseInt(req.params.newsletterId); + + let recipients = await NewsletterHelper.getMailRecipients(newsletterId); + + res.json(recipients); +} + /** * @description create newsletter * @param req {Request} Express req object diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 8ab1b05..8c819b2 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -1,4 +1,3 @@ -import Mail from "nodemailer/lib/mailer"; import { member } from "../entity/club/member/member"; import { newsletter } from "../entity/club/newsletter/newsletter"; import { newsletterDates } from "../entity/club/newsletter/newsletterDates"; @@ -148,6 +147,47 @@ export abstract class NewsletterHelper { } } + public static async getMailRecipients(newsletterId: number) { + let newsletter = await NewsletterService.getById(newsletterId); + let recipients = await NewsletterRecipientsService.getAll(newsletterId); + let config = await NewsletterConfigService.getAll(); + + let allowedForMail = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId); + + const members = await this.transformRecipientsToMembers(newsletter, recipients); + const mailRecipients = members.filter( + (m) => + m.sendNewsletter != null && + m.sendNewsletter?.email != null && + allowedForMail.includes(m.sendNewsletter?.type?.id) + ); + + return mailRecipients; + } + + public static async getPrintRecipients(newsletterId: number) { + let newsletter = await NewsletterService.getById(newsletterId); + let recipients = await NewsletterRecipientsService.getAll(newsletterId); + let config = await NewsletterConfigService.getAll(); + + let notAllowedForPdf = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId); + + const members = await this.transformRecipientsToMembers(newsletter, recipients); + const pdfRecipients = members.filter( + (m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null + ); + + pdfRecipients.unshift({ + id: "0", + firstname: "Alle Mitglieder", + lastname: CLUB_NAME, + nameaffix: "", + salutation: { salutation: "" }, + } as member); + + return pdfRecipients; + } + public static getICSFilePath(newsletter: newsletter) { return FileSystemHelper.formatPath( "newsletter", @@ -163,22 +203,12 @@ export abstract class NewsletterHelper { public static async sendMails(newsletterId: number) { let newsletter = await NewsletterService.getById(newsletterId); let dates = await NewsletterDatesService.getAll(newsletterId); - let recipients = await NewsletterRecipientsService.getAll(newsletterId); - let config = await NewsletterConfigService.getAll(); const { value, error } = CalendarHelper.buildICS(dates.map((r) => r.calendar)); if (error) throw new InternalException("Failed Building ICS form Mail", error); this.saveIcsToFile(newsletter, value); - let allowedForMail = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId); - - const members = await this.transformRecipientsToMembers(newsletter, recipients); - const mailRecipients = members.filter( - (m) => - m.sendNewsletter != null && - m.sendNewsletter?.email != null && - allowedForMail.includes(m.sendNewsletter?.type?.id) - ); + const mailRecipients = await this.getMailRecipients(newsletterId); this.formatJobEmit("progress", "mail", "info", newsletterId, mailRecipients.length, 0, "starting sending"); @@ -231,7 +261,6 @@ export abstract class NewsletterHelper { public static async printPdfs(newsletterId: number) { let newsletter = await NewsletterService.getById(newsletterId); let dates = await NewsletterDatesService.getAll(newsletterId); - let recipients = await NewsletterRecipientsService.getAll(newsletterId); let config = await NewsletterConfigService.getAll(); FileSystemHelper.clearDirectoryByFiletype( @@ -243,26 +272,13 @@ export abstract class NewsletterHelper { if (error) throw new InternalException("Failed Building ICS form Pdf", error); this.saveIcsToFile(newsletter, value); - let notAllowedForPdf = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId); let printWithAdress = config.filter((c) => c.config == NewsletterConfigType.pdf).map((c) => c.comTypeId); - const members = await this.transformRecipientsToMembers(newsletter, recipients); - const pdfRecipients = members.filter( - (m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null - ); + const pdfRecipients = await this.getPrintRecipients(newsletterId); - this.formatJobEmit("progress", "pdf", "info", newsletterId, pdfRecipients.length + 1, 0, "starting sending"); + this.formatJobEmit("progress", "pdf", "info", newsletterId, pdfRecipients.length, 0, "starting sending"); - for (const [index, rec] of [ - ...pdfRecipients, - { - id: "0", - firstname: "Alle Mitglieder", - lastname: CLUB_NAME, - nameaffix: "", - salutation: { salutation: "" }, - } as member, - ].entries()) { + for (const [index, rec] of pdfRecipients.entries()) { let data = this.buildData(newsletter, dates, rec, printWithAdress.includes(rec.sendNewsletter?.type?.id)); await PdfExport.renderFile({ @@ -278,8 +294,8 @@ export abstract class NewsletterHelper { "pdf", "success", newsletterId, - pdfRecipients.length + 1, - index + 1, + pdfRecipients.length, + index, `successfully printed for ${rec.lastname}, ${rec.firstname}` ); }) @@ -289,8 +305,8 @@ export abstract class NewsletterHelper { "pdf", "failed", newsletterId, - pdfRecipients.length + 1, - index + 1, + pdfRecipients.length, + index, `failed print for ${rec.lastname}, ${rec.firstname}` ); }); @@ -307,8 +323,8 @@ export abstract class NewsletterHelper { "pdf", "success", newsletterId, - pdfRecipients.length + 1, - pdfRecipients.length + 1, + pdfRecipients.length, + pdfRecipients.length, "sucessfully combined pdf" ); }) @@ -318,8 +334,8 @@ export abstract class NewsletterHelper { "pdf", "failed", newsletterId, - pdfRecipients.length + 1, - pdfRecipients.length + 1, + pdfRecipients.length, + pdfRecipients.length, "failed combining pdf" ); }); @@ -329,8 +345,8 @@ export abstract class NewsletterHelper { "pdf", "info", newsletterId, - pdfRecipients.length + 1, - pdfRecipients.length + 1, + pdfRecipients.length, + pdfRecipients.length, `completed printing process` ); } diff --git a/src/routes/admin/club/newsletter.ts b/src/routes/admin/club/newsletter.ts index 9a71bbf..5ec1aba 100644 --- a/src/routes/admin/club/newsletter.ts +++ b/src/routes/admin/club/newsletter.ts @@ -16,6 +16,8 @@ import { createNewsletterPrintoutPreviewById, getNewsletterPrintoutProgressById, getNewsletterSendingProgressById, + getNewsletterMailReceiversById, + getNewsletterPrintReceiversById, } from "../../../controller/admin/club/newsletterController"; import PermissionHelper from "../../../helpers/permissionHelper"; @@ -57,6 +59,14 @@ router.get("/:newsletterId/sendprogress", async (req: Request, res: Response) => await getNewsletterSendingProgressById(req, res); }); +router.get("/:newsletterId/printrecipients", async (req: Request, res: Response) => { + await getNewsletterPrintReceiversById(req, res); +}); + +router.get("/:newsletterId/mailrecipients", async (req: Request, res: Response) => { + await getNewsletterMailReceiversById(req, res); +}); + router.post( "/", PermissionHelper.passCheckMiddleware("create", "club", "protocol"), -- 2.45.3 From a660642bbb2db73e52e907934901abeed9635cfd Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 08:07:11 +0200 Subject: [PATCH 05/10] fix: string replacement on all occurances --- .../admin/club/newsletterController.ts | 4 ++-- .../admin/club/protocolController.ts | 2 +- src/helpers/newsletterHelper.ts | 18 +++++++++++------- src/helpers/templateHelper.ts | 6 +++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/controller/admin/club/newsletterController.ts b/src/controller/admin/club/newsletterController.ts index 9338732..5399336 100644 --- a/src/controller/admin/club/newsletterController.ts +++ b/src/controller/admin/club/newsletterController.ts @@ -94,7 +94,7 @@ export async function getNewsletterPrintoutsById(req: Request, res: Response): P let newsletter = await NewsletterService.getById(newsletterId); let filesInFolder = FileSystemHelper.getFilesInDirectory( - `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}` + `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}` ); res.json(filesInFolder); @@ -114,7 +114,7 @@ export async function getNewsletterPrintoutByIdAndPrint(req: Request, res: Respo let filepath = FileSystemHelper.formatPath( "newsletter", - `${newsletter.id}_${newsletter.title.replace(" ", "")}`, + `${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, filename ); diff --git a/src/controller/admin/club/protocolController.ts b/src/controller/admin/club/protocolController.ts index ecae059..98396c8 100644 --- a/src/controller/admin/club/protocolController.ts +++ b/src/controller/admin/club/protocolController.ts @@ -235,7 +235,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P year: "numeric", })}`; - let filename = `${new Date().toISOString().split("T")[0]}_${iteration + 1}_Protokoll_${protocol.title.replace( + let filename = `${new Date().toISOString().split("T")[0]}_${iteration + 1}_Protokoll_${protocol.title.replaceAll( /[^a-zA-Z0-9]/g, "" )}`; diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 8c819b2..3104359 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -191,13 +191,17 @@ export abstract class NewsletterHelper { public static getICSFilePath(newsletter: newsletter) { return FileSystemHelper.formatPath( "newsletter", - `${newsletter.id}_${newsletter.title.replace(" ", "")}`, + `${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, `events.ics` ); } public static saveIcsToFile(newsletter: newsletter, ics: string) { - FileSystemHelper.writeFile(`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, "events.ics", ics); + FileSystemHelper.writeFile( + `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, + "events.ics", + ics + ); } public static async sendMails(newsletterId: number) { @@ -264,7 +268,7 @@ export abstract class NewsletterHelper { let config = await NewsletterConfigService.getAll(); FileSystemHelper.clearDirectoryByFiletype( - `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, + `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, ".pdf" ); @@ -284,8 +288,8 @@ export abstract class NewsletterHelper { await PdfExport.renderFile({ template: "newsletter", title: `Newsletter von ${CLUB_NAME}`, - filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replace(" ", "-"), - folder: `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, + filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"), + folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, data: data, }) .then(() => { @@ -313,9 +317,9 @@ export abstract class NewsletterHelper { } await PdfExport.sqashToSingleFile( - `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, + `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`, "allPdfsTogether", - `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}` + `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}` ) .then(() => { this.formatJobEmit( diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index b8d97f1..2aac38b 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -27,9 +27,9 @@ export abstract class TemplateHelper { } static normalizeTemplate(template: string): string { - template = template.replace(/.*?<\/listend>/g, "{{/each}}"); - template = template.replace(/]*>(WDH Start: )?/g, "{{#each "); - template = template.replace(/<\/liststart>/g, "}}"); + template = template.replaceAll(/.*?<\/listend>/g, "{{/each}}"); + template = template.replaceAll(/]*>(WDH Start: )?/g, "{{#each "); + template = template.replaceAll(/<\/liststart>/g, "}}"); return template; } -- 2.45.3 From e3ff3456ad0713730d6afe54eba6a35790559bb2 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 10:02:32 +0200 Subject: [PATCH 06/10] fix: member commumication update --- .../member/communicationCommandHandler.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/command/club/member/communicationCommandHandler.ts b/src/command/club/member/communicationCommandHandler.ts index 3899394..2890485 100644 --- a/src/command/club/member/communicationCommandHandler.ts +++ b/src/command/club/member/communicationCommandHandler.ts @@ -18,6 +18,17 @@ export default abstract class CommunicationCommandHandler { let insertId = -1; return await dataSource .transaction(async (manager) => { + if (createCommunication.isSendNewsletter) { + await manager + .createQueryBuilder() + .update(communication) + .set({ + isSendNewsletter: false, + }) + .where("memberId = :memberId", { memberId: createCommunication.memberId }) + .execute(); + } + await manager .createQueryBuilder() .insert() @@ -40,16 +51,6 @@ export default abstract class CommunicationCommandHandler { .then((result) => { insertId = result.identifiers[0].id; }); - - await manager - .createQueryBuilder() - .update(communication) - .set({ - isSendNewsletter: false, - }) - .where("memberId = :memberId", { memberId: createCommunication.memberId }) - .andWhere("id <> :id", { id: insertId }) - .execute(); }) .then(() => { return insertId; @@ -67,6 +68,17 @@ export default abstract class CommunicationCommandHandler { static async update(updateCommunication: UpdateCommunicationCommand): Promise { return await dataSource .transaction(async (manager) => { + if (updateCommunication.isSendNewsletter) { + await manager + .createQueryBuilder() + .update(communication) + .set({ + isSendNewsletter: false, + }) + .where("memberId = :memberId", { memberId: updateCommunication.memberId }) + .execute(); + } + await manager .createQueryBuilder() .update(communication) @@ -85,16 +97,6 @@ export default abstract class CommunicationCommandHandler { .where("id = :id", { id: updateCommunication.id }) .andWhere("memberId = :memberId", { memberId: updateCommunication.memberId }) .execute(); - - await manager - .createQueryBuilder() - .update(communication) - .set({ - isSendNewsletter: false, - }) - .where("memberId = :memberId", { memberId: updateCommunication.memberId }) - .andWhere("id <> :id", { id: updateCommunication.id }) - .execute(); }) .then(() => {}) .catch((err) => { -- 2.45.3 From 8e80901c0f1e48625efbf6ab95ef99ce7577fed8 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 12:28:26 +0200 Subject: [PATCH 07/10] optimize: browser open / close at sequential printing --- src/helpers/pdfExport.ts | 88 +++++++++++++++++++++++++---------- src/helpers/templateHelper.ts | 4 +- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/helpers/pdfExport.ts b/src/helpers/pdfExport.ts index e59ba54..429f551 100644 --- a/src/helpers/pdfExport.ts +++ b/src/helpers/pdfExport.ts @@ -1,10 +1,47 @@ -import puppeteer from "puppeteer"; +import puppeteer, { Browser } from "puppeteer"; import { TemplateHelper } from "./templateHelper"; import { PermissionModule } from "../type/permissionTypes"; import { FileSystemHelper } from "./fileSystemHelper"; import { PDFDocument } from "pdf-lib"; +import { StringHelper } from "./stringHelper"; export abstract class PdfExport { + private static browserInstance: undefined | Browser = undefined; + private static timeout: undefined | NodeJS.Timeout = undefined; + private static printing = new Map(); + + private static async renderTemplate( + template: `${PermissionModule}` | `${PermissionModule}.${string}`, + title: string, + data: any, + customTemplate?: { + headerId?: number; + footerId?: number; + bodyId?: string | number; + headerHeight: number; + footerHeight: number; + } + ): Promise<{ header: string; footer: string; body: string; headerMargin?: number; footerMargin?: number }> { + if (!customTemplate) { + return await TemplateHelper.renderFileForModule({ + module: template, + headerData: data, + bodyData: data, + footerData: data, + title: title, + }); + } else { + return await TemplateHelper.renderFileForCustom({ + module: template, + customTemplate, + headerData: data, + bodyData: data, + footerData: data, + title: title, + }); + } + } + static async renderFile({ template, title = "pdf-export FF Admin", @@ -28,33 +65,25 @@ export abstract class PdfExport { footerHeight: number; }; }) { + try { + clearTimeout(this.timeout); + } catch (err) {} + let id = StringHelper.random(32); + this.printing.set(id, "printing"); + if (folder != "") FileSystemHelper.createFolder(folder); - let header: string, footer: string, body: string, headerMargin: number, footerMargin: number; - if (!customTemplate) { - ({ header, footer, body, headerMargin, footerMargin } = await TemplateHelper.renderFileForModule({ - module: template, - headerData: data, - bodyData: data, - footerData: data, - title: title, - })); - } else { - ({ header, footer, body, headerMargin, footerMargin } = await TemplateHelper.renderFileForCustom({ - module: template, - customTemplate, - headerData: data, - bodyData: data, - footerData: data, - title: title, - })); + const renderedTemplate = await this.renderTemplate(template, title, data, customTemplate); + let { header, footer, body, headerMargin, footerMargin } = renderedTemplate; + + if (!this.browserInstance || !this.browserInstance.connected) { + this.browserInstance = await puppeteer.launch({ + headless: true, + args: ["--no-sandbox", "--disable-gpu", "--disable-setuid-sandbox"], + }); } - const browser = await puppeteer.launch({ - headless: true, - args: ["--no-sandbox", "--disable-gpu", "--disable-setuid-sandbox"], - }); - const page = await browser.newPage(); + const page = await this.browserInstance.newPage(); await page.setContent(body, { waitUntil: "domcontentloaded" }); const exportPath = FileSystemHelper.formatPath(folder, `${filename}.pdf`); @@ -74,7 +103,16 @@ export abstract class PdfExport { footerTemplate: footer, }); - await browser.close(); + await page.close(); + + this.printing.delete(id); + + if (this.printing.size == 0) { + this.timeout = setTimeout(() => { + this.browserInstance.close(); + this.browserInstance = undefined; + }, 5000); + } return pdf; } diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts index 2aac38b..3dc5ad8 100644 --- a/src/helpers/templateHelper.ts +++ b/src/helpers/templateHelper.ts @@ -11,7 +11,9 @@ export abstract class TemplateHelper { try { tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`); } catch (err) { - tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template.split(".")[1]}.template.html`); + tmpFile = FileSystemHelper.readTemplateFile( + `/src/templates/${template.split(".")[template.split(".").length - 1]}.template.html` + ); } return tmpFile; } -- 2.45.3 From 2942d9a059ad7a4840b5af0802d9c00d63975abb Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 12:28:41 +0200 Subject: [PATCH 08/10] fix: sort dates in newsletter print --- src/helpers/newsletterHelper.ts | 68 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 3104359..438f06d 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -61,39 +61,41 @@ export abstract class NewsletterHelper { newsletterTitle: newsletter.newsletterTitle, newsletterText: newsletter.newsletterText, newsletterSignatur: newsletter.newsletterSignatur, - dates: dates.map((d) => ({ - title: d.diffTitle || d.calendar.title, - content: d.diffDescription || d.calendar.content, - starttime: d.calendar.starttime, - formattedStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - }), - formattedFullStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }), - endtime: d.calendar.endtime, - formattedEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - }), - formattedFullEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "long", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }), - location: d.calendar.location, - })), + dates: dates + .map((d) => ({ + title: d.diffTitle || d.calendar.title, + content: d.diffDescription || d.calendar.content, + starttime: d.calendar.starttime, + formattedStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + }), + formattedFullStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }), + endtime: d.calendar.endtime, + formattedEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + }), + formattedFullEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", { + weekday: "long", + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }), + location: d.calendar.location, + })) + .sort((a, b) => a.starttime.getTime() - b.starttime.getTime()), ...(recipient ? { recipient: { -- 2.45.3 From 2a7a0ad523329b35d066acf86bdb839aa6d6659b Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 12:32:08 +0200 Subject: [PATCH 09/10] fix: name of job emit --- src/helpers/newsletterHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 438f06d..a62504e 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -282,7 +282,7 @@ export abstract class NewsletterHelper { const pdfRecipients = await this.getPrintRecipients(newsletterId); - this.formatJobEmit("progress", "pdf", "info", newsletterId, pdfRecipients.length, 0, "starting sending"); + this.formatJobEmit("progress", "pdf", "info", newsletterId, pdfRecipients.length, 0, "starting printing"); for (const [index, rec] of pdfRecipients.entries()) { let data = this.buildData(newsletter, dates, rec, printWithAdress.includes(rec.sendNewsletter?.type?.id)); -- 2.45.3 From 2e88c8db547e51930628bf330fb8144002945e0c Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Thu, 10 Apr 2025 12:45:29 +0200 Subject: [PATCH 10/10] undo: count of print --- src/helpers/newsletterHelper.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index a62504e..e84427f 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -300,8 +300,8 @@ export abstract class NewsletterHelper { "pdf", "success", newsletterId, - pdfRecipients.length, - index, + pdfRecipients.length + 1, + index + 1, `successfully printed for ${rec.lastname}, ${rec.firstname}` ); }) @@ -311,8 +311,8 @@ export abstract class NewsletterHelper { "pdf", "failed", newsletterId, - pdfRecipients.length, - index, + pdfRecipients.length + 1, + index + 1, `failed print for ${rec.lastname}, ${rec.firstname}` ); }); @@ -329,8 +329,8 @@ export abstract class NewsletterHelper { "pdf", "success", newsletterId, - pdfRecipients.length, - pdfRecipients.length, + pdfRecipients.length + 1, + pdfRecipients.length + 1, "sucessfully combined pdf" ); }) @@ -340,8 +340,8 @@ export abstract class NewsletterHelper { "pdf", "failed", newsletterId, - pdfRecipients.length, - pdfRecipients.length, + pdfRecipients.length + 1, + pdfRecipients.length + 1, "failed combining pdf" ); }); @@ -351,8 +351,8 @@ export abstract class NewsletterHelper { "pdf", "info", newsletterId, - pdfRecipients.length, - pdfRecipients.length, + pdfRecipients.length + 1, + pdfRecipients.length + 1, `completed printing process` ); } -- 2.45.3