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>
This commit is contained in:
@@ -3,16 +3,18 @@ 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'];
|
||||
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,
|
||||
hasLogo: !!values.company_logo_filename,
|
||||
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,
|
||||
};
|
||||
@@ -21,28 +23,45 @@ export async function getCompanyInfo() {
|
||||
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 }, actorId) {
|
||||
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 values = await settingsRepo.getAll(['company_logo_filename']);
|
||||
if (values.company_logo_filename) {
|
||||
const oldPath = path.join(logoDir, values.company_logo_filename);
|
||||
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
||||
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 = file.mimetype;
|
||||
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(() => {});
|
||||
}
|
||||
if (Object.keys(entries).length) await settingsRepo.setMany(entries);
|
||||
await logAudit({ userId: actorId, action: 'update', entityType: 'settings:company', entityId: null });
|
||||
return getCompanyInfo();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user