Compare commits

..

No commits in common. "bec4c8bf4cc62206ab5d75ef969e0ca4c37d5bd6" and "e8c9a307ab4b917fe180ed62529276a344a292f7" have entirely different histories.

14 changed files with 25 additions and 385 deletions

View File

@ -51,14 +51,6 @@ export const hasPermission = async (user, objectType, action) => {
export const checkPermissions = (objectType, action) => async (req, res, next) => {
try {
if (req.user?._objectType == 'host') {
if (
(objectType === 'documentTemplate' && action === 'design') ||
(objectType === 'file' && action === 'download')
) {
return next();
}
}
const allowed = await hasPermission(req.user, objectType, action);
if (!allowed) {
return res.status(403).json({ error: 'Forbidden', code: 'FORBIDDEN' });

View File

@ -25,31 +25,6 @@ const documentSizeSchema = new Schema(
required: true,
default: false,
},
printPadding: {
type: Boolean,
required: true,
default: false,
},
paddingLeft: {
type: Number,
required: true,
default: 0,
},
paddingRight: {
type: Number,
required: true,
default: 0,
},
paddingTop: {
type: Number,
required: true,
default: 0,
},
paddingBottom: {
type: Number,
required: true,
default: 0,
},
},
{ timestamps: true }
);

View File

@ -63,7 +63,7 @@ const isAuthenticated = async (req, res, next) => {
try {
const session = await getSession(token);
if (session && session.expiresAt > Date.now()) {
req.user = { ...session.user, _objectType: 'user' };
req.user = session.user;
req.session = session;
return next();
}
@ -71,7 +71,7 @@ const isAuthenticated = async (req, res, next) => {
// Try email-render JWT (short-lived token for Puppeteer email notifications)
const user = await lookupUserByToken(token);
if (user) {
req.user = { ...user, _objectType: 'user' };
req.user = user;
req.session = { user };
return next();
}
@ -85,7 +85,6 @@ const isAuthenticated = async (req, res, next) => {
if (hostId && authCode) {
const host = await getObject({ model: hostModel, id: hostId });
if (host && host.authCode === authCode) {
req.user = { ...host, _objectType: 'host' };
return next();
}
}

View File

@ -5,32 +5,8 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
const router = express.Router();
const listAllowedFilters = [
'name',
'width',
'height',
'printPadding',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'createdAt',
'updatedAt',
'_reference',
];
const listAllowedSorters = [
'name',
'width',
'height',
'infiniteHeight',
'printPadding',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'createdAt',
'updatedAt',
];
const listAllowedFilters = ['name', 'width', 'height', 'createdAt', 'updatedAt', '_reference'];
const listAllowedSorters = ['name', 'width', 'height', 'infiniteHeight', 'createdAt', 'updatedAt'];
const propertiesAllowedFilters = [];
import {
listDocumentSizesRouteHandler,

View File

@ -77,34 +77,13 @@ describe('Document Size Service Route Handlers', () => {
describe('newDocumentSizeRouteHandler', () => {
it('should create a new document size', async () => {
req.body = {
name: 'Letter',
width: 216,
height: 279,
infiniteHeight: false,
printPadding: false,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
};
req.body = { name: 'Letter', width: 216, height: 279 };
const mockSize = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockSize);
await newDocumentSizeRouteHandler(req, res);
expect(newObject).toHaveBeenCalledWith(
expect.objectContaining({
newData: expect.objectContaining({
name: 'Letter',
printPadding: false,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
}),
})
);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockSize);
});
});

View File

@ -113,7 +113,7 @@ describe('Document Template Service Route Handlers', () => {
'<Text>Hello</Text>',
{ name: 'Test' },
1,
{ padding: true },
{},
true
);
expect(res.send).toHaveBeenCalledWith(mockPreview);
@ -155,34 +155,12 @@ describe('Document Template Service Route Handlers', () => {
'<Text>Hello</Text>',
{ name: 'Test' },
'pdf',
{ padding: true }
{}
);
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/pdf');
expect(res.send).toHaveBeenCalledWith(pdf);
});
it('passes padding false when requested by the caller', async () => {
req.params.id = 'template-1';
req.query = { type: 'pdf', padding: 'false' };
req.body = { object: { name: 'Test' } };
templateManager.renderDownload.mockResolvedValue({
type: 'pdf',
mime: 'application/pdf',
extension: 'pdf',
buffers: [Buffer.from('pdf-data')],
});
await downloadDocumentTemplateRouteHandler(req, res);
expect(templateManager.renderDownload).toHaveBeenCalledWith(
'template-1',
undefined,
{ name: 'Test' },
'pdf',
{ padding: false }
);
});
it('should return base64 images when multiple pages are rendered', async () => {
req.params.id = 'template-1';
req.query = { type: 'png' };

View File

@ -116,11 +116,6 @@ export const editDocumentSizeRouteHandler = async (req, res) => {
width: req.body.width,
height: req.body.height,
infiniteHeight: req.body.infiniteHeight,
printPadding: req.body.printPadding,
paddingLeft: req.body.paddingLeft,
paddingRight: req.body.paddingRight,
paddingTop: req.body.paddingTop,
paddingBottom: req.body.paddingBottom,
};
// Create audit log before updating
const result = await editObject({
@ -148,11 +143,6 @@ export const newDocumentSizeRouteHandler = async (req, res) => {
width: req.body.width,
height: req.body.height,
infiniteHeight: req.body.infiniteHeight,
printPadding: req.body.printPadding,
paddingLeft: req.body.paddingLeft,
paddingRight: req.body.paddingRight,
paddingTop: req.body.paddingTop,
paddingBottom: req.body.paddingBottom,
};
const result = await newObject({
model: documentSizeModel,

View File

@ -20,27 +20,6 @@ import { formatTemplateContent } from '../../templates/templateformatter.js';
const logger = log4js.getLogger('Document Templates');
logger.level = config.server.logLevel;
function parseOptionalBoolean(value) {
if (value === undefined || value === null || value === '') {
return undefined;
}
if (value === true || value === false) {
return value;
}
const lower = String(value).trim().toLowerCase();
if (lower === 'true' || lower === '1') {
return true;
}
if (lower === 'false' || lower === '0') {
return false;
}
return undefined;
}
function getPaddingOption(req, defaultValue = true) {
return parseOptionalBoolean(req.body?.padding ?? req.query?.padding) ?? defaultValue;
}
export const listDocumentTemplatesRouteHandler = async (
req,
res,
@ -314,7 +293,6 @@ export const previewDocumentTemplateRouteHandler = async (req, res) => {
const content = req.body?.content;
const testObject = req.body?.testObject || req.body?.object || {};
const scale = req.body?.scale ?? 1;
const padding = getPaddingOption(req, true);
logger.debug(`Previewing document template with ID: ${id}`);
@ -323,7 +301,7 @@ export const previewDocumentTemplateRouteHandler = async (req, res) => {
content,
testObject,
scale,
{ padding },
{},
true
);
@ -342,9 +320,7 @@ export const downloadDocumentTemplateRouteHandler = async (req, res) => {
const data = req.body?.object || req.body?.testObject || {};
const width = req.body?.width || req.query.width;
const filename = req.body?.filename || req.query.filename || 'document';
const options = {
padding: getPaddingOption(req, true),
};
const options = {};
if (width) {
options.width = Number(width);
}

View File

@ -59,25 +59,4 @@ describe('formatTemplateContent', () => {
expect(result.content).not.toMatch(/\(\s+<%/);
expect(result.content).not.toMatch(/%>\s+\)/);
});
it('should not add a space next to a colon before EJS output', () => {
const input =
'<Barcode format="code128" height="33px" barcodeWidth="3">INV: <%= _reference %>\n </Barcode>';
const result = formatTemplateContent(input);
expect(result.content).toBe(
'<Barcode format="code128" height="33px" barcodeWidth="3">INV:<%= _reference %></Barcode>'
);
});
it('should keep barcode content and the closing tag on one line', () => {
const input =
'<Barcode format="code128" height="33px" barcodeWidth="3">INV:<%= _reference %></Barcode>';
const result = formatTemplateContent(input);
expect(result.content).toBe(
'<Barcode format="code128" height="33px" barcodeWidth="3">INV:<%= _reference %></Barcode>'
);
expect(result.content).not.toMatch(/%>\s*\n\s*<\/Barcode>/);
});
});

View File

@ -91,51 +91,11 @@ jest.unstable_mockModule('../../config.js', () => ({
},
}));
const { TemplateManager, resolveDocumentPadding } = await import('../templatemanager.js');
const { TemplateManager } = await import('../templatemanager.js');
const { getObject } = await import('../../database/database.js');
const ejs = (await import('ejs')).default;
const { generatePDF } = await import('../pdffactory.js');
describe('resolveDocumentPadding', () => {
const paddedSize = {
printPadding: false,
paddingLeft: 10,
paddingRight: 8,
paddingTop: 6,
paddingBottom: 4,
};
it('does not apply padding unless requested or printPadding is true', () => {
expect(resolveDocumentPadding(paddedSize, false)).toEqual({
applyPadding: false,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
});
});
it('applies padding when requested', () => {
expect(resolveDocumentPadding(paddedSize, true)).toEqual({
applyPadding: true,
paddingLeft: 10,
paddingRight: 8,
paddingTop: 6,
paddingBottom: 4,
});
});
it('applies padding when printPadding is true even if not requested', () => {
expect(resolveDocumentPadding({ ...paddedSize, printPadding: true }, false)).toEqual({
applyPadding: true,
paddingLeft: 10,
paddingRight: 8,
paddingTop: 6,
paddingBottom: 4,
});
});
});
describe('TemplateManager', () => {
let templateManager;
@ -201,87 +161,6 @@ describe('TemplateManager', () => {
expect(generatePDF).toHaveBeenCalled();
expect(result.pdf).toBeDefined();
});
it('adds padding to page size when padding is requested', async () => {
getObject.mockResolvedValue({
documentSize: {
width: 100,
height: 50,
infiniteHeight: false,
printPadding: false,
paddingLeft: 10,
paddingRight: 10,
paddingTop: 5,
paddingBottom: 5,
},
global: false,
objectType: 'printer',
});
await templateManager.renderPDF('temp-id', 'content', {}, { padding: true });
expect(generatePDF).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
width: 120,
height: 60,
})
);
});
it('adds padding when printPadding is true even if padding is false', async () => {
getObject.mockResolvedValue({
documentSize: {
width: 100,
height: 50,
infiniteHeight: false,
printPadding: true,
paddingLeft: 10,
paddingRight: 10,
paddingTop: 5,
paddingBottom: 5,
},
global: false,
objectType: 'printer',
});
await templateManager.renderPDF('temp-id', 'content', {}, { padding: false });
expect(generatePDF).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
width: 120,
height: 60,
})
);
});
it('keeps the original page size when padding is not applied', async () => {
getObject.mockResolvedValue({
documentSize: {
width: 100,
height: 50,
infiniteHeight: false,
printPadding: false,
paddingLeft: 10,
paddingRight: 10,
paddingTop: 5,
paddingBottom: 5,
},
global: false,
objectType: 'printer',
});
await templateManager.renderPDF('temp-id', 'content', {}, { padding: false });
expect(generatePDF).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
width: 100,
height: 50,
})
);
});
});
describe('renderDownload', () => {

View File

@ -18,22 +18,18 @@
}
.previewWrapper {
width: <%= (scaledPageWidth) || (scaledWidth) || '50mm' %>;
height: <%= (scaledPageHeight) || (scaledHeight) || '50mm' %>;
width: <%= (scaledWidth) || '50mm' %>;
height: <%= (scaledHeight) || '50mm' %>;
}
.previewDocument {
width: <%= (width) || '50mm' %>;
height: <%= (height) || '50mm' %>;
padding: <%= (paddingTop) || '0mm' %> <%= (paddingRight) || '0mm' %> <%= (paddingBottom) || '0mm' %> <%= (paddingLeft) || '0mm' %>;
box-sizing: content-box;
transform: scale(<%= scale || '1' %>);
transform-origin: top left;
}
.renderDocument {
width: <%= (width) || '50mm' %>;
height: <%= (height) || '50mm' %>;
padding: <%= (paddingTop) || '0mm' %> <%= (paddingRight) || '0mm' %> <%= (paddingBottom) || '0mm' %> <%= (paddingLeft) || '0mm' %>;
box-sizing: content-box;
width: <%= (scaledWidth) || '50mm' %>;
height: <%= (scaledHeight) || '50mm' %>;
transform: scale(<%= scale || '1' %>);
}
</style>

View File

@ -25,11 +25,6 @@ body {
transform-origin: top left;
position: relative;
overflow: hidden;
box-sizing: content-box;
}
.renderDocument {
box-sizing: content-box;
}
.documentText {

View File

@ -4,9 +4,7 @@ const { html: beautifyHtml, js: beautifyJs } = beautify;
const EJS_BLOCK_REGEX = /<%[\s\S]*?%>/g;
const EJS_TAG_REGEX = /^<%([=#-]?)([\s\S]*?)%>/;
const EJS_COMMENT_PLACEHOLDER_REGEX = /<!--EJS_PH_(\d+)-->/g;
const EJS_INLINE_PLACEHOLDER_REGEX = /___EJS_PH_(\d+)___/g;
const TEXT_ONLY_TAG_REGEX = /<([A-Za-z][\w:-]*)(\s[^>]*)?>([^<]*)<\/\1>/g;
const EJS_PLACEHOLDER_REGEX = /<!--EJS_PH_(\d+)-->/g;
const HTML_BEAUTIFY_OPTIONS = {
indent_size: 2,
@ -49,53 +47,28 @@ function formatEjsTag(tag) {
}
}
function isControlEjs(tag) {
const match = tag.match(EJS_TAG_REGEX);
if (!match) {
return false;
}
const [, modifier] = match;
return !modifier;
}
function maskEjsBlocks(content) {
const ejsBlocks = [];
const masked = content.replace(EJS_BLOCK_REGEX, (match) => {
const index = ejsBlocks.length;
ejsBlocks.push(match);
if (isControlEjs(match)) {
return `\n<!--EJS_PH_${index}-->\n`;
}
return `___EJS_PH_${index}___`;
return `\n<!--EJS_PH_${index}-->\n`;
});
return { masked, ejsBlocks };
}
function restoreEjsBlocks(content, ejsBlocks) {
const restore = (_, index) => formatEjsTag(ejsBlocks[Number(index)]);
return content
.replace(EJS_COMMENT_PLACEHOLDER_REGEX, restore)
.replace(EJS_INLINE_PLACEHOLDER_REGEX, restore);
}
function collapseTextOnlyTags(content) {
return content.replace(TEXT_ONLY_TAG_REGEX, (_, tagName, attrs, inner) => {
const collapsedInner = inner.replace(/^\s+|\s+$/g, '');
return `<${tagName}${attrs || ''}>${collapsedInner}</${tagName}>`;
});
return content.replace(EJS_PLACEHOLDER_REGEX, (_, index) =>
formatEjsTag(ejsBlocks[Number(index)])
);
}
function tightenParenthesesAroundEjs(content) {
return content.replace(/\(\s+(<%)/g, '($1').replace(/(%>)\s+\)/g, '$1)');
}
function tightenColonsAroundEjs(content) {
return content.replace(/:\s+(<%)/g, ':$1');
}
export function formatTemplateContent(content) {
if (content == null || typeof content !== 'string') {
return { error: 'Content is required and must be a string.', code: 400 };
@ -108,10 +81,8 @@ export function formatTemplateContent(content) {
try {
const { masked, ejsBlocks } = maskEjsBlocks(content);
let formatted = beautifyHtml(masked, HTML_BEAUTIFY_OPTIONS);
formatted = collapseTextOnlyTags(formatted);
formatted = restoreEjsBlocks(formatted, ejsBlocks);
formatted = tightenParenthesesAroundEjs(formatted);
formatted = tightenColonsAroundEjs(formatted);
return { content: formatted.trimEnd() };
} catch (error) {
return { error: error.message || 'Failed to format template content.', code: 400 };

View File

@ -357,32 +357,6 @@ function imageToSvg(pngBuffer, widthPx, heightPx, documentWidthMm, documentHeigh
</svg>`;
}
function toPaddingMm(value) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
}
export function resolveDocumentPadding(documentSize = {}, paddingRequested = false) {
const applyPadding = documentSize.printPadding == true || paddingRequested == true;
if (!applyPadding) {
return {
applyPadding: false,
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
};
}
return {
applyPadding: true,
paddingLeft: toPaddingMm(documentSize.paddingLeft),
paddingRight: toPaddingMm(documentSize.paddingRight),
paddingTop: toPaddingMm(documentSize.paddingTop),
paddingBottom: toPaddingMm(documentSize.paddingBottom),
};
}
export class TemplateManager {
constructor() {
this.fc = {
@ -394,10 +368,9 @@ export class TemplateManager {
async renderTemplate(id, content, data = {}, scale = 1, options = {}, preview = true) {
try {
const { padding: paddingRequested = false, ...ejsOptions } = options;
const defaultOptions = {
async: true,
...ejsOptions,
...options,
};
logger.debug('Rendering template:', id);
@ -504,11 +477,6 @@ export class TemplateManager {
}
const infiniteHeight = documentSize.infiniteHeight == true;
const padding = resolveDocumentPadding(documentSize, paddingRequested == true);
const pageWidth = (documentSize.width || 0) + padding.paddingLeft + padding.paddingRight;
const pageHeight = infiniteHeight
? null
: (documentSize.height || 0) + padding.paddingTop + padding.paddingBottom;
const baseHtml = await ejs.render(
baseTemplate,
@ -518,12 +486,6 @@ export class TemplateManager {
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
scaledWidth: `${documentSize.width * scale}mm`,
scaledHeight: infiniteHeight ? 'auto' : `${documentSize.height * scale}mm`,
scaledPageWidth: `${pageWidth * scale}mm`,
scaledPageHeight: infiniteHeight ? 'auto' : `${pageHeight * scale}mm`,
paddingTop: `${padding.paddingTop}mm`,
paddingRight: `${padding.paddingRight}mm`,
paddingBottom: `${padding.paddingBottom}mm`,
paddingLeft: `${padding.paddingLeft}mm`,
scale: `${scale}`,
baseCSS: baseCSS,
previewPaginationScript: preview ? previewPaginationScript : '',
@ -533,14 +495,9 @@ export class TemplateManager {
return {
html: baseHtml,
width: pageWidth,
height: infiniteHeight ? 'auto' : pageHeight,
width: documentSize.width,
height: infiniteHeight ? 'auto' : documentSize.height,
infiniteHeight: infiniteHeight,
printPadding: documentSize.printPadding == true,
paddingLeft: padding.paddingLeft,
paddingRight: padding.paddingRight,
paddingTop: padding.paddingTop,
paddingBottom: padding.paddingBottom,
};
} catch (error) {
console.error(error);
@ -591,13 +548,12 @@ export class TemplateManager {
try {
logger.debug(`Rendering ${format} for template:`, id);
const { padding: _padding, ...imageOptions } = options;
const pdfResult = await this.renderPDF(id, content, data, options);
if (pdfResult.error != undefined) {
return { error: pdfResult.error, code: pdfResult.code };
}
const images = await convertPDFToImage(pdfResult.pdf, imageOptions);
const images = await convertPDFToImage(pdfResult.pdf, options);
const converted = [];
for (const image of images) {
if (format === 'png') {
@ -617,13 +573,12 @@ export class TemplateManager {
try {
logger.debug('Rendering SVG for template:', id);
const { padding: _padding, ...imageOptions } = options;
const pdfResult = await this.renderPDF(id, content, data, options);
if (pdfResult.error != undefined) {
return { error: pdfResult.error, code: pdfResult.code };
}
const images = await convertPDFToImage(pdfResult.pdf, imageOptions);
const images = await convertPDFToImage(pdfResult.pdf, options);
const svgs = [];
for (const image of images) {
const png = await sharp(image).png().toBuffer();