feat: nombre y logo de empresa configurables (solo dev, no desplegado a prod)

Nueva marca configurable desde Configuracion -> Empresa (solo superadmin): nombre y logo,
guardados en system_settings (mismo patron que SMTP, sin tabla nueva). Nuevo endpoint publico
GET /api/company (+ /logo) para que login/registro/recuperacion de clave puedan mostrar la marca
antes de iniciar sesion. Sin configurar, se ve "LinkedCorp" y el logo estatico de siempre.

Probado en dev (build + Playwright: panel de Configuracion, sidebar expandido/compacto, login
publico), sin desplegar a produccion por pedido explicito del usuario -- este commit sirve de
punto de control en git antes de decidir el despliegue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-15 14:42:15 -05:00
parent 7ba4a89d49
commit 2a1bdf1ba9
27 changed files with 336 additions and 37 deletions
+17
View File
@@ -0,0 +1,17 @@
import path from 'node:path';
import { ok, asyncHandler, HttpError } from '../../utils/http.js';
import * as service from './service.js';
// Publico (sin auth): lo necesitan las paginas de login/registro/recuperacion antes de iniciar sesion.
export const getCompany = asyncHandler(async (req, res) => ok(res, await service.getCompanyInfo()));
export const getCompanyLogo = asyncHandler(async (req, res) => {
const file = await service.getCompanyLogoFile();
if (!file) throw new HttpError(404, 'No hay logo configurado');
// sendFile exige ruta absoluta (a diferencia de res.download, que la resuelve sola).
res.type(file.mime).sendFile(path.resolve(file.filePath));
});
export const putCompany = asyncHandler(async (req, res) => {
ok(res, await service.updateCompanyInfo({ name: req.body.name, file: req.file }, req.currentUser.id));
});
+14
View File
@@ -0,0 +1,14 @@
import { Router } from 'express';
import { requireAuth, requireRole, CAN_MANAGE_SETTINGS } from '../../middleware/auth.js';
import { uploadLogo } from '../../middleware/uploadLogo.js';
import { getCompany, getCompanyLogo, putCompany } from './controller.js';
const router = Router();
// GET publico (sin requireAuth): las paginas de login/registro/recuperacion lo necesitan antes
// de tener sesion. Solo la escritura exige superadmin.
router.get('/', getCompany);
router.get('/logo', getCompanyLogo);
router.put('/', requireAuth, requireRole(...CAN_MANAGE_SETTINGS), uploadLogo.single('logo'), putCompany);
export default router;
+48
View File
@@ -0,0 +1,48 @@
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 { HttpError } from '../../utils/http.js';
const COMPANY_KEYS = ['company_name', 'company_logo_filename', 'company_logo_mime'];
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,
// 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;
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) {
const entries = {};
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);
}
entries.company_logo_filename = file.filename;
entries.company_logo_mime = file.mimetype;
}
if (Object.keys(entries).length) await settingsRepo.setMany(entries);
await logAudit({ userId: actorId, action: 'update', entityType: 'settings:company', entityId: null });
return getCompanyInfo();
}