From 6cb8ca0a125b2412c2a431b219e221a4fb66df1c Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Mon, 23 Dec 2024 14:00:50 +0100 Subject: [PATCH] template preparation --- src/command/templateUsageCommand.ts | 6 ++ src/command/templateUsageCommandHandler.ts | 28 +++++++ src/controller/admin/protocolController.ts | 2 +- .../admin/templateUsageController.ts | 64 ++++++++++++++++ src/data-source.ts | 4 + src/entity/member.ts | 2 +- src/entity/templateUsage.ts | 39 ++++++++++ src/factory/admin/templateUsage.ts | 27 +++++++ src/helpers/pdfExport.ts | 30 +++----- src/helpers/templateHelper.ts | 76 +++++++++++++++++++ src/migrations/1734949173739-templateUsage.ts | 75 ++++++++++++++++++ src/routes/admin/index.ts | 2 + src/routes/admin/templateUsage.ts | 21 +++++ src/service/templateUsageService.ts | 46 +++++++++++ src/type/permissionTypes.ts | 5 +- src/viewmodel/admin/templateUsage.models.ts | 8 ++ 16 files changed, 414 insertions(+), 21 deletions(-) create mode 100644 src/command/templateUsageCommand.ts create mode 100644 src/command/templateUsageCommandHandler.ts create mode 100644 src/controller/admin/templateUsageController.ts create mode 100644 src/entity/templateUsage.ts create mode 100644 src/factory/admin/templateUsage.ts create mode 100644 src/helpers/templateHelper.ts create mode 100644 src/migrations/1734949173739-templateUsage.ts create mode 100644 src/routes/admin/templateUsage.ts create mode 100644 src/service/templateUsageService.ts create mode 100644 src/viewmodel/admin/templateUsage.models.ts diff --git a/src/command/templateUsageCommand.ts b/src/command/templateUsageCommand.ts new file mode 100644 index 0000000..b9218a9 --- /dev/null +++ b/src/command/templateUsageCommand.ts @@ -0,0 +1,6 @@ +export interface UpdateTemplateUsageCommand { + scope: string; + headerId: number | null; + bodyId: number | null; + footerId: number | null; +} diff --git a/src/command/templateUsageCommandHandler.ts b/src/command/templateUsageCommandHandler.ts new file mode 100644 index 0000000..54626ba --- /dev/null +++ b/src/command/templateUsageCommandHandler.ts @@ -0,0 +1,28 @@ +import { dataSource } from "../data-source"; +import { templateUsage } from "../entity/templateUsage"; +import InternalException from "../exceptions/internalException"; +import { UpdateTemplateUsageCommand } from "./templateUsageCommand"; + +export default abstract class TemplateUsageCommandHandler { + /** + * @description update templateUsage + * @param UpdateTemplateUsageCommand + * @returns {Promise} + */ + static async update(updateTemplateUsage: UpdateTemplateUsageCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(templateUsage) + .set({ + headerId: updateTemplateUsage.headerId, + bodyId: updateTemplateUsage.bodyId, + footerId: updateTemplateUsage.footerId, + }) + .where("scope = :scope", { scope: updateTemplateUsage.scope }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed updating templateUsage", err); + }); + } +} diff --git a/src/controller/admin/protocolController.ts b/src/controller/admin/protocolController.ts index 12a5b22..ef5ca3d 100644 --- a/src/controller/admin/protocolController.ts +++ b/src/controller/admin/protocolController.ts @@ -237,7 +237,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P )}`; await PdfExport.renderFile({ - template: "protocol.template.html", + template: "protocol", title, filename, data: { diff --git a/src/controller/admin/templateUsageController.ts b/src/controller/admin/templateUsageController.ts new file mode 100644 index 0000000..0d146d7 --- /dev/null +++ b/src/controller/admin/templateUsageController.ts @@ -0,0 +1,64 @@ +import { Request, Response } from "express"; +import TemplateUsageService from "../../service/templateUsageService"; +import TemplateUsageFactory from "../../factory/admin/templateUsage"; +import { UpdateTemplateUsageCommand } from "../../command/templateUsageCommand"; +import TemplateUsageCommandHandler from "../../command/templateUsageCommandHandler"; +import PermissionHelper from "../../helpers/permissionHelper"; +import ForbiddenRequestException from "../../exceptions/forbiddenRequestException"; +import { PermissionModule } from "../../type/permissionTypes"; + +/** + * @description get all templateUsages + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getAllTemplateUsages(req: Request, res: Response): Promise { + let templateUsages = await TemplateUsageService.getAll(); + + if (!req.isOwner) { + templateUsages = templateUsages.filter((tu) => { + return ( + PermissionHelper.can(req.permissions, "update", "settings", tu.scope) || + PermissionHelper.can(req.permissions, "update", "club", tu.scope) + ); + }); + } + + res.json(TemplateUsageFactory.mapToBase(templateUsages)); +} + +/** + * @description update templateUsage + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function updateTemplateUsage(req: Request, res: Response): Promise { + const scope = req.params.scope; + let allowedSettings = PermissionHelper.can( + req.permissions, + "update", + "settings", + req.params.scope as PermissionModule + ); + let allowedClub = PermissionHelper.can(req.permissions, "update", "club", req.params.scope as PermissionModule); + + if (!(req.isOwner || allowedSettings || allowedClub)) { + throw new ForbiddenRequestException(`missing permission for editing scope ${req.params.scope}`); + } + + const headerId = req.body.headerId ?? null; + const bodyId = req.body.bodyId ?? null; + const footerId = req.body.footerId ?? null; + + let updateTemplateUsage: UpdateTemplateUsageCommand = { + scope: scope, + headerId: headerId, + bodyId: bodyId, + footerId: footerId, + }; + await TemplateUsageCommandHandler.update(updateTemplateUsage); + + res.sendStatus(204); +} diff --git a/src/data-source.ts b/src/data-source.ts index 0b29b2c..d9567fd 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -53,6 +53,8 @@ import { membershipView } from "./views/membershipsView"; import { MemberDataViews1734520998539 } from "./migrations/1734520998539-memberDataViews"; import { template } from "./entity/template"; import { Template1734854680201 } from "./migrations/1734854680201-template"; +import { templateUsage } from "./entity/templateUsage"; +import { TemplateUsage1734949173739 } from "./migrations/1734949173739-templateUsage"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -93,6 +95,7 @@ const dataSource = new DataSource({ calendarType, query, template, + templateUsage, memberView, memberExecutivePositionsView, memberQualificationsView, @@ -116,6 +119,7 @@ const dataSource = new DataSource({ QueryStore1734187754677, MemberDataViews1734520998539, Template1734854680201, + TemplateUsage1734949173739, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/member.ts b/src/entity/member.ts index 7bb0d78..de7f612 100644 --- a/src/entity/member.ts +++ b/src/entity/member.ts @@ -40,7 +40,7 @@ export class member { birthdate: Date; @OneToMany(() => communication, (communications) => communications.member) - communications: communication; + communications: communication[]; @OneToOne(() => communication, { nullable: true, diff --git a/src/entity/templateUsage.ts b/src/entity/templateUsage.ts new file mode 100644 index 0000000..8aefd11 --- /dev/null +++ b/src/entity/templateUsage.ts @@ -0,0 +1,39 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { template } from "./template"; +import { PermissionModule } from "../type/permissionTypes"; + +@Entity() +export class templateUsage { + @PrimaryColumn({ type: "varchar", length: 255 }) + scope: PermissionModule; + + @Column({ type: "number", nullable: true }) + headerId: number | null; + + @Column({ type: "number", nullable: true }) + bodyId: number | null; + + @Column({ type: "number", nullable: true }) + footerId: number | null; + + @ManyToOne(() => template, { + nullable: true, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + header: template | null; + + @ManyToOne(() => template, { + nullable: true, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + body: template | null; + + @ManyToOne(() => template, { + nullable: true, + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + footer: template | null; +} diff --git a/src/factory/admin/templateUsage.ts b/src/factory/admin/templateUsage.ts new file mode 100644 index 0000000..e2a5acb --- /dev/null +++ b/src/factory/admin/templateUsage.ts @@ -0,0 +1,27 @@ +import { templateUsage } from "../../entity/templateUsage"; +import { TemplateUsageViewModel } from "../../viewmodel/admin/templateUsage.models"; + +export default abstract class TemplateUsageFactory { + /** + * @description map record to templateUsage + * @param {templateUsage} record + * @returns {TemplateUsageViewModel} + */ + public static mapToSingle(record: templateUsage): TemplateUsageViewModel { + return { + scope: record.scope, + header: record.header ? { id: record.header.id, template: record.header.template } : null, + body: record.body ? { id: record.body.id, template: record.body.template } : null, + footer: record.footer ? { id: record.footer.id, template: record.footer.template } : null, + }; + } + + /** + * @description map records to templateUsage + * @param {Array} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/helpers/pdfExport.ts b/src/helpers/pdfExport.ts index c42b185..ff15fad 100644 --- a/src/helpers/pdfExport.ts +++ b/src/helpers/pdfExport.ts @@ -1,36 +1,34 @@ -import { readFileSync } from "fs"; -import Handlebars from "handlebars"; import puppeteer from "puppeteer"; +import { TemplateHelper } from "./templateHelper"; +import { PermissionModule } from "../type/permissionTypes"; export abstract class PdfExport { - static getTemplate(template: string) { - return readFileSync(process.cwd() + "/src/templates/" + template, "utf8"); - } - static async renderFile({ template, title = "pdf-export Mitgliederverwaltung", filename, data, }: { - template: string; + template: PermissionModule; title: string; filename: string; data: any; }) { - const templateHtml = this.getTemplate(template); - const templateCompiled = Handlebars.compile(templateHtml); - const html = templateCompiled(data); + const { header, footer, body } = await TemplateHelper.renderFileForModule({ + module: template, + bodyData: data, + title: title, + }); const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox", "--disable-gpu", "--disable-setuid-sandbox"], }); const page = await browser.newPage(); - await page.setContent(html, { waitUntil: "domcontentloaded" }); + await page.setContent(body, { waitUntil: "domcontentloaded" }); await page.pdf({ - path: process.cwd() + `/export/${filename}.pdf`, // Name der PDF-Datei + path: process.cwd() + `/export/${filename}.pdf`, format: "A4", printBackground: false, margin: { @@ -40,12 +38,8 @@ export abstract class PdfExport { right: "10mm", }, displayHeaderFooter: true, - headerTemplate: `

${title}

`, - footerTemplate: ` -
- Seite von -
- `, + headerTemplate: header, + footerTemplate: footer, }); await browser.close(); diff --git a/src/helpers/templateHelper.ts b/src/helpers/templateHelper.ts new file mode 100644 index 0000000..a8c4dc0 --- /dev/null +++ b/src/helpers/templateHelper.ts @@ -0,0 +1,76 @@ +import { readFileSync } from "fs"; +import TemplateService from "../service/templateService"; +import { PermissionModule } from "../type/permissionTypes"; +import TemplateUsageService from "../service/templateUsageService"; +import Handlebars from "handlebars"; + +export abstract class TemplateHelper { + static getTemplateFromFile(template: string) { + return readFileSync(`${process.cwd()}/src/templates/${template}.template.html`, "utf8"); + } + + static async getTemplateFromStore(templateId: number): Promise { + return (await TemplateService.getById(templateId)).html; + } + + static applyDataToTemplate(template: string, data: any): string { + const normalizedTemplate = this.normalizeTemplate(template); + const templateCompiled = Handlebars.compile(normalizedTemplate); + return templateCompiled(data); + } + + static normalizeTemplate(template: string): string { + template = template.replace(/.*?<\/listend>/g, "{{/each}}"); + template = template.replace(/]*>(WDH Start: )?/g, "{{#each "); + template = template.replace(/<\/liststart>/g, "}}"); + + return template; + } + + static async renderFileForModule({ + module, + title = "pdf-export Mitgliederverwaltung", + headerData = {}, + bodyData = {}, + footerData = {}, + }: { + module: PermissionModule; + title?: string; + headerData?: any; + bodyData?: any; + footerData?: any; + }): Promise<{ header: string; body: string; footer: string }> { + const moduleTemplates = await TemplateUsageService.getByScope(module); + + let header = `

${title}

`; + let footer = ` +
+ Seite von +
+ `; + let body = ""; + + if (moduleTemplates.headerId) { + header = await this.getTemplateFromStore(moduleTemplates.headerId); + header = this.applyDataToTemplate(header, headerData); + } + + if (moduleTemplates.footerId) { + footer = await this.getTemplateFromStore(moduleTemplates.footerId); + footer = this.applyDataToTemplate(footer, footerData); + } + + if (moduleTemplates.bodyId) { + body = await this.getTemplateFromStore(moduleTemplates.bodyId); + } else { + body = this.getTemplateFromFile(module); + } + body = this.applyDataToTemplate(body, bodyData); + + return { + header, + footer, + body, + }; + } +} diff --git a/src/migrations/1734949173739-templateUsage.ts b/src/migrations/1734949173739-templateUsage.ts new file mode 100644 index 0000000..2f419d4 --- /dev/null +++ b/src/migrations/1734949173739-templateUsage.ts @@ -0,0 +1,75 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; +import { DB_TYPE } from "../env.defaults"; +import { templateUsage } from "../entity/templateUsage"; + +export class TemplateUsage1734949173739 implements MigrationInterface { + name = "TemplateUsage1734949173739"; + + public async up(queryRunner: QueryRunner): Promise { + const variableType_int = DB_TYPE == "mysql" ? "int" : "integer"; + + await queryRunner.createTable( + new Table({ + name: "template_usage", + columns: [ + { name: "scope", type: "varchar", length: "255", isPrimary: true }, + { name: "headerId", type: variableType_int, isNullable: true }, + { name: "bodyId", type: variableType_int, isNullable: true }, + { name: "footerId", type: variableType_int, isNullable: true }, + ], + }), + true + ); + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(templateUsage) + .values({ scope: "protocol" }) + .orIgnore() + .execute(); + + await queryRunner.createForeignKey( + "template_usage", + new TableForeignKey({ + columnNames: ["headerId"], + referencedColumnNames: ["id"], + referencedTableName: "template", + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + ); + await queryRunner.createForeignKey( + "template_usage", + new TableForeignKey({ + columnNames: ["bodyId"], + referencedColumnNames: ["id"], + referencedTableName: "template", + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + ); + await queryRunner.createForeignKey( + "template_usage", + new TableForeignKey({ + columnNames: ["footerId"], + referencedColumnNames: ["id"], + referencedTableName: "template", + onDelete: "RESTRICT", + onUpdate: "RESTRICT", + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + const template_usage = await queryRunner.getTable("template_usage"); + let foreignKey = template_usage.foreignKeys.find((fk) => fk.columnNames.indexOf("headerId") !== -1); + await queryRunner.dropForeignKey("template_usage", foreignKey); + foreignKey = template_usage.foreignKeys.find((fk) => fk.columnNames.indexOf("bodyId") !== -1); + await queryRunner.dropForeignKey("template_usage", foreignKey); + foreignKey = template_usage.foreignKeys.find((fk) => fk.columnNames.indexOf("footerId") !== -1); + await queryRunner.dropForeignKey("template_usage", foreignKey); + + await queryRunner.dropTable("template_usage"); + } +} diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index c51f08c..f1c6982 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -9,6 +9,7 @@ import qualification from "./qualification"; import calendarType from "./calendarType"; import queryStore from "./queryStore"; import template from "./template"; +import templateUsage from "./templateUsage"; import member from "./member"; import protocol from "./protocol"; @@ -41,6 +42,7 @@ router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "setti router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), calendarType); router.use("/querystore", PermissionHelper.passCheckMiddleware("read", "settings", "query_store"), queryStore); router.use("/template", PermissionHelper.passCheckMiddleware("read", "settings", "template"), template); +router.use("/templateusage", PermissionHelper.passCheckMiddleware("read", "settings", "template_usage"), templateUsage); router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "member"), member); router.use("/protocol", PermissionHelper.passCheckMiddleware("read", "club", "protocol"), protocol); diff --git a/src/routes/admin/templateUsage.ts b/src/routes/admin/templateUsage.ts new file mode 100644 index 0000000..ca4da5b --- /dev/null +++ b/src/routes/admin/templateUsage.ts @@ -0,0 +1,21 @@ +import express, { Request, Response } from "express"; +import PermissionHelper from "../../helpers/permissionHelper"; +import { getAllTemplateUsages, updateTemplateUsage } from "../../controller/admin/templateUsageController"; +import { PermissionModule } from "../../type/permissionTypes"; +import ForbiddenRequestException from "../../exceptions/forbiddenRequestException"; + +var router = express.Router({ mergeParams: true }); + +router.get("/", async (req: Request, res: Response) => { + await getAllTemplateUsages(req, res); +}); + +router.patch( + "/:scope", + PermissionHelper.passCheckMiddleware("update", "settings", "template_usage"), + async (req: Request, res: Response) => { + await updateTemplateUsage(req, res); + } +); + +export default router; diff --git a/src/service/templateUsageService.ts b/src/service/templateUsageService.ts new file mode 100644 index 0000000..25b61ee --- /dev/null +++ b/src/service/templateUsageService.ts @@ -0,0 +1,46 @@ +import { dataSource } from "../data-source"; +import { templateUsage } from "../entity/templateUsage"; +import InternalException from "../exceptions/internalException"; + +export default abstract class TemplateUsageService { + /** + * @description get all templateUsages + * @returns {Promise>} + */ + static async getAll(): Promise> { + return await dataSource + .getRepository(templateUsage) + .createQueryBuilder("templateUsage") + .leftJoinAndSelect("templateUsage.header", "headerTemplate") + .leftJoinAndSelect("templateUsage.body", "bodyTemplate") + .leftJoinAndSelect("templateUsage.footer", "footerTemplate") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("templates not found", err); + }); + } + + /** + * @description get template by scope + * @returns {Promise} + */ + static async getByScope(scope: string): Promise { + return await dataSource + .getRepository(templateUsage) + .createQueryBuilder("templateUsage") + .leftJoinAndSelect("templateUsage.header", "headerTemplate") + .leftJoinAndSelect("templateUsage.body", "bodyTemplate") + .leftJoinAndSelect("templateUsage.footer", "footerTemplate") + .where("templateUsage.scope = :scope", { scope: scope }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err): null => { + return null; + }); + } +} diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index 62fbc51..34e2be9 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -15,7 +15,8 @@ export type PermissionModule = | "role" | "query" | "query_store" - | "template"; + | "template" + | "template_usage"; export type PermissionType = "read" | "create" | "update" | "delete"; @@ -55,6 +56,7 @@ export const permissionModules: Array = [ "query", "query_store", "template", + "template_usage", ]; export const permissionTypes: Array = ["read", "create", "update", "delete"]; export const sectionsAndModules: SectionsAndModulesObject = { @@ -68,6 +70,7 @@ export const sectionsAndModules: SectionsAndModulesObject = { "calendar_type", "query_store", "template", + "template_usage", ], user: ["user", "role"], }; diff --git a/src/viewmodel/admin/templateUsage.models.ts b/src/viewmodel/admin/templateUsage.models.ts new file mode 100644 index 0000000..3f9aa00 --- /dev/null +++ b/src/viewmodel/admin/templateUsage.models.ts @@ -0,0 +1,8 @@ +import { PermissionModule } from "../../type/permissionTypes"; + +export interface TemplateUsageViewModel { + scope: PermissionModule; + header: { id: number; template: string } | null; + body: { id: number; template: string } | null; + footer: { id: number; template: string } | null; +}