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;