e65111a7f4
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>
68 lines
3.1 KiB
JavaScript
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();
|
|
}
|