CentralBackend/controller/admin/CompanySettingsController.js
2026-05-17 16:15:47 +02:00

137 lines
4.4 KiB
JavaScript

const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository');
const { logger } = require('../../middleware/logger');
const repo = new CompanySettingsRepository();
const ALLOWED_LOGO_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']);
const MAX_LOGO_BYTES = 1024 * 1024;
function normalizeBase64ImageInput(rawValue, rawMimeType) {
if (rawValue === undefined && rawMimeType === undefined) {
return { provided: false };
}
if (rawValue === null || rawValue === '') {
return { provided: true, base64: null, mimeType: null };
}
if (typeof rawValue !== 'string') {
throw new Error('company_logo_base64 must be a string or null');
}
let value = rawValue.trim();
let mimeType = typeof rawMimeType === 'string' ? rawMimeType.trim() : '';
if (!value) {
return { provided: true, base64: null, mimeType: null };
}
const dataUriMatch = value.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\r\n]+)$/);
if (dataUriMatch) {
mimeType = dataUriMatch[1];
value = dataUriMatch[2];
}
const compactBase64 = value.replace(/\s+/g, '');
if (!mimeType) {
throw new Error('company_logo_mime_type is required when company_logo_base64 is provided');
}
if (!ALLOWED_LOGO_MIME_TYPES.has(mimeType)) {
throw new Error(`Unsupported company logo type '${mimeType}'`);
}
if (!/^[A-Za-z0-9+/=]+$/.test(compactBase64)) {
throw new Error('company_logo_base64 is not valid base64');
}
const bytes = Buffer.from(compactBase64, 'base64').length;
if (bytes > MAX_LOGO_BYTES) {
throw new Error('Company logo exceeds 1 MB');
}
return {
provided: true,
base64: compactBase64,
mimeType,
};
}
class CompanySettingsController {
static async get(req, res) {
try {
const settings = await repo.get();
return res.json(settings || {
company_name: '',
company_street: '',
company_postal_city: '',
company_country: '',
company_logo_base64: null,
company_logo_mime_type: null,
});
} catch (err) {
return res.status(500).json({ message: 'Failed to load company settings' });
}
}
static async update(req, res) {
try {
const body = req.body || {};
const requestId = req.id;
const contentType = (req.get('content-type') || '').toLowerCase();
const contentLength = req.get('content-length') || null;
if (contentType.includes('multipart/form-data')) {
logger.warn('companySettings:update:multipart_not_supported', {
requestId,
contentType: req.get('content-type') || null,
hint: 'Send JSON (application/json) or add multer middleware for this route.'
});
}
if (Object.keys(body).length === 0 && contentLength && Number(contentLength) > 0) {
logger.warn('companySettings:update:empty_parsed_body', {
requestId,
contentType: req.get('content-type') || null,
contentLength,
hint: 'Body parser may not match the request content-type or size limit.'
});
}
// Accept both snake_case and camelCase
const payload = {
company_name: body.company_name ?? body.companyName,
company_street: body.company_street ?? body.companyStreet,
company_postal_city: body.company_postal_city ?? body.companyPostalCity,
company_country: body.company_country ?? body.companyCountry,
};
const normalizedLogo = normalizeBase64ImageInput(
body.company_logo_base64 ?? body.companyLogoBase64,
body.company_logo_mime_type ?? body.companyLogoMimeType,
);
if (normalizedLogo.provided) {
payload.company_logo_base64 = normalizedLogo.base64;
payload.company_logo_mime_type = normalizedLogo.mimeType;
}
// Only forward keys that were actually provided (so we don't wipe values on partial updates)
const provided = {};
for (const [key, value] of Object.entries(payload)) {
if (value !== undefined) provided[key] = value;
}
const updated = await repo.update(provided);
return res.json(updated);
} catch (err) {
logger.error('companySettings:update:failed', {
requestId: req && req.id,
message: err?.message,
stack: err?.stack,
});
return res.status(500).json({ message: 'Failed to update company settings' });
}
}
}
module.exports = CompanySettingsController;