const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository'); const { logger } = require('../../middleware/logger'); const crypto = require('crypto'); const repo = new CompanySettingsRepository(); class CompanySettingsController { static _normalizeBase64ImageInput(value) { if (value === undefined) return undefined; if (value === null) return null; const stripDataUri = (str) => { const s = String(str || '').trim(); if (!s) return ''; const m = s.match(/^data:([^;]+);base64,(.*)$/i); return m ? (m[2] || '').trim() : s; }; if (typeof value === 'string') { // If base64 came via urlencoded forms, '+' often becomes ' ' after decoding. // Restoring spaces back to '+' makes the payload usable again. let normalized = stripDataUri(value); if (normalized.includes(' ') && !normalized.includes('+')) { normalized = normalized.replace(/ /g, '+'); } normalized = normalized.replace(/\s+/g, ''); return normalized || null; } if (typeof value === 'object') { // Common Node.js JSON shape for Buffers: { type: 'Buffer', data: [..bytes..] } if (value && value.type === 'Buffer' && Array.isArray(value.data)) { try { const buf = Buffer.from(value.data); const asBase64 = buf.toString('base64'); return asBase64 || null; } catch (_) { return undefined; } } // Common frontend shapes: // { kind: 'base64', base64: '...' } // { kind: 'base64', data: '...' } // { dataUrl: 'data:image/png;base64,...' } // { value: '...' } const candidates = [ value.base64, value.data, value.value, value.content, value.full, value.raw, value.payload, value.src, value.b64, value.base64String, value.dataUrl, value.dataURL, value.data_uri, value.dataURI, value.uri, ]; for (const c of candidates) { if (typeof c === 'string' && c.trim()) { let normalized = stripDataUri(c); if (normalized.includes(' ') && !normalized.includes('+')) { normalized = normalized.replace(/ /g, '+'); } normalized = normalized.replace(/\s+/g, ''); return normalized || null; } } // Fallback: scan shallow object for a base64-ish string const looksLikeImageBase64 = (str) => { const s = String(str || '').trim(); return s.startsWith('data:image/') || s.startsWith('iVBORw0KGgo') || s.startsWith('/9j/') || s.startsWith('R0lGOD'); }; for (const v of Object.values(value)) { if (typeof v === 'string' && looksLikeImageBase64(v)) { let normalized = stripDataUri(v); if (normalized.includes(' ') && !normalized.includes('+')) { normalized = normalized.replace(/ /g, '+'); } normalized = normalized.replace(/\s+/g, ''); return normalized || null; } if (v && typeof v === 'object') { for (const vv of Object.values(v)) { if (typeof vv === 'string' && looksLikeImageBase64(vv)) { let normalized = stripDataUri(vv); if (normalized.includes(' ') && !normalized.includes('+')) { normalized = normalized.replace(/ /g, '+'); } normalized = normalized.replace(/\s+/g, ''); return normalized || null; } } } } } // Unknown shape; ignore instead of persisting garbage return undefined; } static async get(req, res) { try { const settings = await repo.get(); return res.json(settings || { company_name: '', company_street: '', company_postal_city: '', company_country: '', qr_code_60_base64: null, qr_code_120_base64: 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.' }); } const summarizeValue = (val) => { const t = val === null ? 'null' : Array.isArray(val) ? 'array' : typeof val; const summary = { type: t }; if (t === 'string') { const s = String(val); summary.length = s.length; summary.hasDataUriPrefix = s.trim().toLowerCase().startsWith('data:image/'); summary.hasWhitespace = /\s/.test(s); summary.startsWithPngSig = s.trim().startsWith('iVBORw0KGgo'); } else if (t === 'array') { summary.length = val.length; } else if (t === 'object' && val) { summary.keys = Object.keys(val).slice(0, 30); if (val.type === 'Buffer' && Array.isArray(val.data)) { summary.bufferLike = true; summary.dataLength = val.data.length; } } return summary; }; const hash12 = (s) => { try { if (typeof s !== 'string' || !s) return null; return crypto.createHash('sha256').update(s).digest('hex').slice(0, 12); } catch { return null; } }; // Log request shape (not the base64 itself) const incoming60Raw = body.qr_code_60_base64 ?? body.qrCode60Base64; const incoming120Raw = body.qr_code_120_base64 ?? body.qrCode120Base64; if (incoming60Raw !== undefined || incoming120Raw !== undefined) { logger.info('companySettings:update:incoming_qr', { requestId, contentType: req.get('content-type') || null, contentLength, bodyKeys: Object.keys(body).slice(0, 50), qr60: summarizeValue(incoming60Raw), qr120: summarizeValue(incoming120Raw), }); } // 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, qr_code_60_base64: CompanySettingsController._normalizeBase64ImageInput(body.qr_code_60_base64 ?? body.qrCode60Base64), qr_code_120_base64: CompanySettingsController._normalizeBase64ImageInput(body.qr_code_120_base64 ?? body.qrCode120Base64), }; // 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; } // Debug without leaking base64 if (incoming60Raw !== undefined && provided.qr_code_60_base64 === undefined) { logger.warn('companySettings:update:qr60_ignored', { requestId, incoming: summarizeValue(incoming60Raw), reason: 'normalize_returned_undefined_or_unrecognized_shape' }); } if (incoming120Raw !== undefined && provided.qr_code_120_base64 === undefined) { logger.warn('companySettings:update:qr120_ignored', { requestId, incoming: summarizeValue(incoming120Raw), reason: 'normalize_returned_undefined_or_unrecognized_shape' }); } if (provided.qr_code_60_base64 !== undefined || provided.qr_code_120_base64 !== undefined) { const len60 = typeof provided.qr_code_60_base64 === 'string' ? provided.qr_code_60_base64.length : null; const len120 = typeof provided.qr_code_120_base64 === 'string' ? provided.qr_code_120_base64.length : null; logger.info('companySettings:update:qr_normalized', { requestId, has60: provided.qr_code_60_base64 !== undefined, type60: provided.qr_code_60_base64 === null ? 'null' : typeof provided.qr_code_60_base64, len60, sha60: typeof provided.qr_code_60_base64 === 'string' ? hash12(provided.qr_code_60_base64) : null, has120: provided.qr_code_120_base64 !== undefined, type120: provided.qr_code_120_base64 === null ? 'null' : typeof provided.qr_code_120_base64, len120, sha120: typeof provided.qr_code_120_base64 === 'string' ? hash12(provided.qr_code_120_base64) : null, }); } const updated = await repo.update(provided); if (updated && (provided.qr_code_60_base64 !== undefined || provided.qr_code_120_base64 !== undefined)) { const storedLen60 = typeof updated.qr_code_60_base64 === 'string' ? updated.qr_code_60_base64.length : null; const storedLen120 = typeof updated.qr_code_120_base64 === 'string' ? updated.qr_code_120_base64.length : null; logger.info('companySettings:update:qr_stored', { requestId, storedLen60, storedSha60: typeof updated.qr_code_60_base64 === 'string' ? hash12(updated.qr_code_60_base64) : null, storedLen120, storedSha120: typeof updated.qr_code_120_base64 === 'string' ? hash12(updated.qr_code_120_base64) : null, }); } 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;