CentralBackend/controller/admin/CompanySettingsController.js
2026-03-16 20:03:54 +01:00

268 lines
10 KiB
JavaScript

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;