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:
@@ -0,0 +1,11 @@
|
||||
import { HttpError } from './http.js';
|
||||
|
||||
export function requireEditVersion(value) {
|
||||
if (!Number.isSafeInteger(value) || value < 1) throw new HttpError(428, 'Falta la version de edicion');
|
||||
return value;
|
||||
}
|
||||
|
||||
export function requireUpdated(row) {
|
||||
if (!row) throw new HttpError(409, 'El registro cambio en el servidor. Recarga antes de guardar');
|
||||
return row;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
|
||||
}
|
||||
|
||||
export function invitationEmail(url) {
|
||||
const safeUrl = escapeHtml(url);
|
||||
return `<p>Te agregaron como miembro de un proyecto en project.linkedcorp.net.</p>
|
||||
<p>Completa tu registro para acceder (elige tu nombre y una clave):</p>
|
||||
<p><a href="${safeUrl}">${safeUrl}</a></p><p>Este enlace vence en 7 dias.</p>`;
|
||||
}
|
||||
|
||||
export function verificationEmail(name, url) {
|
||||
const safeUrl = escapeHtml(url);
|
||||
return `<p>Hola ${escapeHtml(name)},</p><p>Confirma tu correo para continuar tu registro:</p><p><a href="${safeUrl}">${safeUrl}</a></p><p>Este enlace vence en 24 horas.</p>`;
|
||||
}
|
||||
|
||||
export function recoveryEmail(name, url) {
|
||||
const safeUrl = escapeHtml(url);
|
||||
return `<p>Hola ${escapeHtml(name)},</p><p>Para elegir una nueva clave entra a este enlace (vence en 1 hora):</p><p><a href="${safeUrl}">${safeUrl}</a></p><p>Si no pediste esto, ignora el correo.</p>`;
|
||||
}
|
||||
+13
-13
@@ -4,24 +4,24 @@ import { getSmtpConfigInternal } from '../modules/settings/service.js';
|
||||
// Degradacion a modo dev cuando el SMTP no esta configurado (regla del orquestador): en vez de
|
||||
// fallar, se loguea el contenido/enlace y el flujo que llamo a esto sigue funcionando.
|
||||
export async function sendMail({ to, subject, html }) {
|
||||
const config = await getSmtpConfigInternal();
|
||||
try {
|
||||
const config = await getSmtpConfigInternal();
|
||||
|
||||
if (!config) {
|
||||
console.log(`[mail:dev] to=${to} subject="${subject}"\n${html}`);
|
||||
return { sent: false, dev: true };
|
||||
}
|
||||
if (!config) {
|
||||
console.log(`[mail:dev] to=${to} subject="${subject}"\n${html}`);
|
||||
return { sent: false, dev: true };
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: { user: config.user, pass: config.pass },
|
||||
});
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: { user: config.user, pass: config.pass },
|
||||
});
|
||||
|
||||
// Nunca dejar que un fallo de envio (SMTP mal configurado, host caido) tumbe al que llama —
|
||||
// critico para forgot-password, que siempre debe responder generico sin importar el resultado.
|
||||
try {
|
||||
await transporter.sendMail({ from: config.from, to, subject, html });
|
||||
return { sent: true, dev: false };
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function redactUrl(url = '') {
|
||||
const value = String(url);
|
||||
try {
|
||||
const parsed = new URL(value, 'http://local.invalid');
|
||||
for (const key of parsed.searchParams.keys()) {
|
||||
if (key.toLowerCase() === 'token') parsed.searchParams.set(key, '[REDACTED]');
|
||||
}
|
||||
return parsed.pathname + parsed.search + parsed.hash;
|
||||
} catch {
|
||||
return value.replace(/([?&]token=)[^&#]*/gi, '$1[REDACTED]');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user