Agregar firmas externas seguras y mejorar entregabilidad de correos
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
const db = require('../../config/db');
|
||||
|
||||
const ensureSchema = async () => {
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS invitation_flows (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
document_id INT NOT NULL,
|
||||
owner_user_id INT NOT NULL,
|
||||
base_signature_id INT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
current_signed_filename VARCHAR(255) NOT NULL,
|
||||
current_signed_path VARCHAR(500) NOT NULL,
|
||||
signature_style VARCHAR(32) NOT NULL DEFAULT 'qr',
|
||||
qr_position VARCHAR(16) NULL,
|
||||
qr_background VARCHAR(16) NULL,
|
||||
qr_border VARCHAR(16) NULL,
|
||||
signature_image_base64 MEDIUMTEXT NULL,
|
||||
total_invites INT NOT NULL DEFAULT 0,
|
||||
completed_invites INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_invitation_flows_document_owner (document_id, owner_user_id),
|
||||
INDEX idx_invitation_flows_status (status)
|
||||
)
|
||||
`);
|
||||
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS invitation_signers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
flow_id INT NOT NULL,
|
||||
owner_user_id INT NOT NULL,
|
||||
document_id INT NOT NULL,
|
||||
order_index INT NOT NULL,
|
||||
invitee_name VARCHAR(160) NOT NULL,
|
||||
invitee_email VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
token_expires DATETIME NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'queued',
|
||||
legal_accepted_at DATETIME NULL,
|
||||
viewed_at DATETIME NULL,
|
||||
sent_at DATETIME NULL,
|
||||
signed_at DATETIME NULL,
|
||||
signed_filename VARCHAR(255) NULL,
|
||||
signed_path VARCHAR(500) NULL,
|
||||
certificate_subject_cn VARCHAR(255) NULL,
|
||||
certificate_issuer_cn VARCHAR(255) NULL,
|
||||
verification_code VARCHAR(100) NULL,
|
||||
verification_url VARCHAR(500) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_invitation_signers_token (token),
|
||||
UNIQUE KEY uq_invitation_signers_flow_order (flow_id, order_index),
|
||||
INDEX idx_invitation_signers_document_owner (document_id, owner_user_id),
|
||||
INDEX idx_invitation_signers_status (status)
|
||||
)
|
||||
`);
|
||||
};
|
||||
|
||||
const createFlow = async ({
|
||||
documentId,
|
||||
ownerUserId,
|
||||
baseSignatureId,
|
||||
currentSignedFilename,
|
||||
currentSignedPath,
|
||||
signatureStyle,
|
||||
qrPosition,
|
||||
qrBackground,
|
||||
qrBorder,
|
||||
signatureImageBase64,
|
||||
totalInvites,
|
||||
}) => {
|
||||
const [result] = await db.execute(
|
||||
`INSERT INTO invitation_flows
|
||||
(document_id, owner_user_id, base_signature_id, current_signed_filename, current_signed_path,
|
||||
signature_style, qr_position, qr_background, qr_border, signature_image_base64, total_invites)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
documentId,
|
||||
ownerUserId,
|
||||
baseSignatureId,
|
||||
currentSignedFilename,
|
||||
currentSignedPath,
|
||||
signatureStyle,
|
||||
qrPosition || null,
|
||||
qrBackground || null,
|
||||
qrBorder || null,
|
||||
signatureImageBase64 || null,
|
||||
totalInvites,
|
||||
]
|
||||
);
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
const createInvitation = async ({
|
||||
flowId,
|
||||
ownerUserId,
|
||||
documentId,
|
||||
orderIndex,
|
||||
inviteeName,
|
||||
inviteeEmail,
|
||||
token,
|
||||
tokenExpires,
|
||||
status,
|
||||
}) => {
|
||||
const [result] = await db.execute(
|
||||
`INSERT INTO invitation_signers
|
||||
(flow_id, owner_user_id, document_id, order_index, invitee_name, invitee_email, token, token_expires, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[flowId, ownerUserId, documentId, orderIndex, inviteeName, inviteeEmail, token, tokenExpires, status]
|
||||
);
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
const findFlowByDocumentOwner = async (documentId, ownerUserId) => {
|
||||
const [rows] = await db.execute(
|
||||
'SELECT * FROM invitation_flows WHERE document_id = ? AND owner_user_id = ? ORDER BY id DESC LIMIT 1',
|
||||
[documentId, ownerUserId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
};
|
||||
|
||||
const findFlowById = async (id) => {
|
||||
const [rows] = await db.execute('SELECT * FROM invitation_flows WHERE id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
};
|
||||
|
||||
const listInvitationsByFlow = async (flowId) => {
|
||||
const [rows] = await db.execute(
|
||||
'SELECT * FROM invitation_signers WHERE flow_id = ? ORDER BY order_index ASC',
|
||||
[flowId]
|
||||
);
|
||||
return rows;
|
||||
};
|
||||
|
||||
const getMaxOrderIndex = async (flowId) => {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT COALESCE(MAX(order_index), 0) AS maxOrder
|
||||
FROM invitation_signers
|
||||
WHERE flow_id = ?`,
|
||||
[flowId]
|
||||
);
|
||||
return Number(rows[0]?.maxOrder || 0);
|
||||
};
|
||||
|
||||
const findInvitationByIdForOwner = async (id, ownerUserId) => {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT i.*, f.status as flow_status, f.current_signed_filename, f.current_signed_path,
|
||||
f.signature_style, f.qr_position, f.qr_background, f.qr_border, f.signature_image_base64,
|
||||
f.base_signature_id, f.total_invites, f.completed_invites,
|
||||
d.title as document_title, d.filename as document_filename,
|
||||
u.name as owner_name, u.email as owner_email
|
||||
FROM invitation_signers i
|
||||
JOIN invitation_flows f ON f.id = i.flow_id
|
||||
JOIN documents d ON d.id = i.document_id
|
||||
JOIN users u ON u.id = i.owner_user_id
|
||||
WHERE i.id = ? AND i.owner_user_id = ?
|
||||
LIMIT 1`,
|
||||
[id, ownerUserId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
};
|
||||
|
||||
const findInvitationByToken = async (token) => {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT i.*, f.status as flow_status, f.current_signed_filename, f.current_signed_path,
|
||||
f.signature_style, f.qr_position, f.qr_background, f.qr_border, f.signature_image_base64,
|
||||
f.base_signature_id, f.total_invites, f.completed_invites,
|
||||
d.title as document_title, d.filename as document_filename,
|
||||
u.name as owner_name, u.email as owner_email
|
||||
FROM invitation_signers i
|
||||
JOIN invitation_flows f ON f.id = i.flow_id
|
||||
JOIN documents d ON d.id = i.document_id
|
||||
JOIN users u ON u.id = i.owner_user_id
|
||||
WHERE i.token = ?
|
||||
LIMIT 1`,
|
||||
[token]
|
||||
);
|
||||
return rows[0] || null;
|
||||
};
|
||||
|
||||
const markInvitationSent = async (id) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_signers
|
||||
SET status = 'sent',
|
||||
sent_at = COALESCE(sent_at, NOW())
|
||||
WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
};
|
||||
|
||||
const markInvitationViewed = async (id) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_signers
|
||||
SET status = CASE WHEN status = 'sent' THEN 'opened' ELSE status END,
|
||||
viewed_at = COALESCE(viewed_at, NOW())
|
||||
WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
};
|
||||
|
||||
const markInvitationTermsAccepted = async (id) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_signers
|
||||
SET legal_accepted_at = COALESCE(legal_accepted_at, NOW())
|
||||
WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
};
|
||||
|
||||
const markInvitationSigned = async ({
|
||||
id,
|
||||
signedFilename,
|
||||
signedPath,
|
||||
certificateSubjectCn,
|
||||
certificateIssuerCn,
|
||||
verificationCode,
|
||||
verificationUrl,
|
||||
}) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_signers
|
||||
SET status = 'signed',
|
||||
signed_at = NOW(),
|
||||
signed_filename = ?,
|
||||
signed_path = ?,
|
||||
certificate_subject_cn = ?,
|
||||
certificate_issuer_cn = ?,
|
||||
verification_code = ?,
|
||||
verification_url = ?
|
||||
WHERE id = ?`,
|
||||
[signedFilename, signedPath, certificateSubjectCn || null, certificateIssuerCn || null, verificationCode || null, verificationUrl || null, id]
|
||||
);
|
||||
};
|
||||
|
||||
const invalidateInvitationToken = async (id, replacementToken) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_signers
|
||||
SET token = ?, token_expires = NOW()
|
||||
WHERE id = ?`,
|
||||
[replacementToken, id]
|
||||
);
|
||||
};
|
||||
|
||||
const activateInvitation = async (id) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_signers
|
||||
SET status = 'sent',
|
||||
sent_at = COALESCE(sent_at, NOW())
|
||||
WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
};
|
||||
|
||||
const updateFlowProgress = async ({ flowId, currentSignedFilename, currentSignedPath, completedInvites, status }) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_flows
|
||||
SET current_signed_filename = ?,
|
||||
current_signed_path = ?,
|
||||
completed_invites = ?,
|
||||
status = ?
|
||||
WHERE id = ?`,
|
||||
[currentSignedFilename, currentSignedPath, completedInvites, status, flowId]
|
||||
);
|
||||
};
|
||||
|
||||
const findNextInvitation = async (flowId) => {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT * FROM invitation_signers
|
||||
WHERE flow_id = ? AND status = 'queued'
|
||||
ORDER BY order_index ASC
|
||||
LIMIT 1`,
|
||||
[flowId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
};
|
||||
|
||||
const countSignedInvitations = async (flowId) => {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT COUNT(*) AS total
|
||||
FROM invitation_signers
|
||||
WHERE flow_id = ? AND status = 'signed'`,
|
||||
[flowId]
|
||||
);
|
||||
return Number(rows[0]?.total || 0);
|
||||
};
|
||||
|
||||
const countInvitations = async (flowId) => {
|
||||
const [rows] = await db.execute(
|
||||
`SELECT COUNT(*) AS total
|
||||
FROM invitation_signers
|
||||
WHERE flow_id = ?`,
|
||||
[flowId]
|
||||
);
|
||||
return Number(rows[0]?.total || 0);
|
||||
};
|
||||
|
||||
const updateFlowCounts = async ({ flowId, totalInvites, completedInvites, status }) => {
|
||||
await db.execute(
|
||||
`UPDATE invitation_flows
|
||||
SET total_invites = ?,
|
||||
completed_invites = ?,
|
||||
status = ?
|
||||
WHERE id = ?`,
|
||||
[totalInvites, completedInvites, status, flowId]
|
||||
);
|
||||
};
|
||||
|
||||
const removeInvitation = async (id) => {
|
||||
await db.execute('DELETE FROM invitation_signers WHERE id = ?', [id]);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ensureSchema,
|
||||
createFlow,
|
||||
createInvitation,
|
||||
findFlowByDocumentOwner,
|
||||
findFlowById,
|
||||
listInvitationsByFlow,
|
||||
getMaxOrderIndex,
|
||||
findInvitationByIdForOwner,
|
||||
findInvitationByToken,
|
||||
markInvitationSent,
|
||||
markInvitationViewed,
|
||||
markInvitationTermsAccepted,
|
||||
markInvitationSigned,
|
||||
invalidateInvitationToken,
|
||||
activateInvitation,
|
||||
updateFlowProgress,
|
||||
findNextInvitation,
|
||||
countSignedInvitations,
|
||||
countInvitations,
|
||||
updateFlowCounts,
|
||||
removeInvitation,
|
||||
};
|
||||
Reference in New Issue
Block a user