334 lines
9.7 KiB
JavaScript
334 lines
9.7 KiB
JavaScript
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,
|
|
};
|