137 lines
4.4 KiB
JavaScript
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;
|