Files
project.linkedcorp.net/backend/src/modules/company/service.js
T
Milton Gonzalez e65111a7f4 feat: version 3.0.0 -- Correcciones de auditoria de seguridad, accesibilidad y redundancia
Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico,
revocacion de JWT por version de sesion (028), control optimista de edicion (029),
logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion
de proyectos, accesibilidad de dialogos/controles e i18n.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:36:02 -05:00

68 lines
3.1 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import * as settingsRepo from '../settings/repository.js';
import { logoDir } from '../../middleware/uploadLogo.js';
import { logAudit } from '../../utils/audit.js';
import { requireEditVersion, requireUpdated } from '../../utils/editVersion.js';
import { HttpError } from '../../utils/http.js';
const COMPANY_KEYS = ['company_name', 'company_logo_filename', 'company_logo_mime', 'company_edit_version'];
const DEFAULT_NAME = 'LinkedCorp';
export async function getCompanyInfo() {
const values = await settingsRepo.getAll(COMPANY_KEYS);
return {
name: values.company_name || DEFAULT_NAME,
edit_version: Number(values.company_edit_version),
hasLogo: !!values.company_logo_filename && ['image/png', 'image/jpeg', 'image/webp'].includes(values.company_logo_mime),
// El propio nombre de archivo (unico por subida) sirve de version para cache-busting del logo.
logoVersion: values.company_logo_filename || null,
};
}
export async function getCompanyLogoFile() {
const values = await settingsRepo.getAll(['company_logo_filename', 'company_logo_mime']);
if (!values.company_logo_filename) return null;
if (!['image/png', 'image/jpeg', 'image/webp'].includes(values.company_logo_mime)) return null;
const filePath = path.join(logoDir, values.company_logo_filename);
if (!fs.existsSync(filePath)) return null;
return { filePath, mime: values.company_logo_mime || 'application/octet-stream' };
}
export async function updateCompanyInfo({ name, file, editVersion }, actorId) {
requireEditVersion(editVersion);
const entries = {};
let oldLogoFilename;
if (name !== undefined) {
const trimmed = (name || '').trim();
if (!trimmed) throw new HttpError(400, 'El nombre de la empresa es obligatorio');
entries.company_name = trimmed;
}
if (file) {
const bytes = await fs.promises.readFile(file.path);
const png = bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
const jpeg = bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
const webp = bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP';
const actualMime = png ? 'image/png' : jpeg ? 'image/jpeg' : webp ? 'image/webp' : null;
if (!actualMime || file.mimetype !== actualMime) {
await fs.promises.unlink(file.path).catch(() => {});
throw new HttpError(400, 'El contenido del logo no coincide con PNG, JPG o WEBP');
}
const values = await settingsRepo.getAll(['company_logo_filename']);
oldLogoFilename = values.company_logo_filename;
entries.company_logo_filename = file.filename;
entries.company_logo_mime = actualMime;
}
try {
requireUpdated(await settingsRepo.setManyIfVersion(entries, 'company_edit_version', editVersion));
} catch (err) {
if (file) await fs.promises.unlink(file.path).catch(() => {});
throw err;
}
if (file && oldLogoFilename && oldLogoFilename !== file.filename) {
await fs.promises.unlink(path.join(logoDir, oldLogoFilename)).catch(() => {});
}
await logAudit({ userId: actorId, action: 'update', entityType: 'settings:company', entityId: null });
return getCompanyInfo();
}