fix: refactor asaas webhook for reliability and security
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 54s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 54s
Details
This commit is contained in:
parent
a8f439c1cd
commit
094b9e5707
95
server.ts
95
server.ts
|
|
@ -1127,70 +1127,81 @@ app.post('/api/admin/reorder', authenticateToken, requireAdmin, (req: Authentica
|
||||||
// --- ASAAS WEBHOOK HANDLER ---
|
// --- ASAAS WEBHOOK HANDLER ---
|
||||||
app.post('/api/webhooks/asaas', (req: Request, res: Response) => {
|
app.post('/api/webhooks/asaas', (req: Request, res: Response) => {
|
||||||
const db = loadDb();
|
const db = loadDb();
|
||||||
const configuredSecret = db.settings?.asaasWebhookSecret || process.env.ASAAS_WEBHOOK_TOKEN || 'devflix_webhook_secret_key';
|
const { event, payment } = req.body;
|
||||||
|
|
||||||
const tokenHeader = req.headers['asaas-token'] || req.headers['authorization'];
|
// 1. Logging Antecipado (para debuggar mesmo se der 401)
|
||||||
|
const logId = `wh_log_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
if (event) {
|
||||||
|
db.webhookLogs.push({
|
||||||
|
id: logId,
|
||||||
|
event,
|
||||||
|
payload: JSON.stringify(req.body),
|
||||||
|
receivedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
saveDb(db); // Salva o log imediatamente para não perdê-lo se der return early
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validação de Segurança
|
||||||
|
const configuredSecret = db.settings?.asaasWebhookSecret || process.env.ASAAS_WEBHOOK_TOKEN || 'devflix_webhook_secret_key';
|
||||||
|
// O Asaas usa asaas-access-token por padrão
|
||||||
|
const tokenHeader = req.headers['asaas-access-token'] || req.headers['asaas-token'] || req.headers['authorization'];
|
||||||
|
|
||||||
if (configuredSecret && tokenHeader !== configuredSecret) {
|
if (configuredSecret && tokenHeader !== configuredSecret) {
|
||||||
console.warn(`[WEBHOOK SEGURANÇA] Token inválido bloqueado: ${tokenHeader}`);
|
console.warn(`[WEBHOOK SEGURANÇA] Token inválido bloqueado: ${tokenHeader}`);
|
||||||
res.status(401).json({ error: 'Não autorizado - Token de Webhook do Asaas é inválido ou ausente.' });
|
res.status(401).json({ error: 'Não autorizado - Token de Webhook do Asaas é inválido ou ausente.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { event, payment } = req.body;
|
|
||||||
|
|
||||||
if (!event || !payment) {
|
if (!event || !payment) {
|
||||||
res.status(400).json({ error: 'Formato do Webhook inválido' });
|
res.status(400).json({ error: 'Formato do Webhook inválido' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the webhook event
|
// 3. Processamento do Pagamento
|
||||||
const logId = `wh_log_${Math.random().toString(36).substring(2, 9)}`;
|
|
||||||
db.webhookLogs.push({
|
|
||||||
id: logId,
|
|
||||||
event,
|
|
||||||
payload: JSON.stringify(req.body),
|
|
||||||
receivedAt: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find user by Asaas Customer ID
|
|
||||||
const customerId = payment.customer;
|
const customerId = payment.customer;
|
||||||
const user = db.users.find(u => u.asaasCustomerId === customerId);
|
const user = db.users.find(u => u.asaasCustomerId === customerId);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
// Process payment updates
|
// Busca APENAS pelo ID exato da fatura
|
||||||
let userPaymentIdx = db.payments.findIndex(p => p.id === payment.id || (p.userId === user.id && p.status === 'PENDING'));
|
let userPaymentIdx = db.payments.findIndex(p => p.id === payment.id);
|
||||||
|
|
||||||
|
const isPaid = event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED';
|
||||||
|
const isOverdue = event === 'PAYMENT_OVERDUE';
|
||||||
|
const isRefunded = event === 'PAYMENT_REFUNDED' || event === 'PAYMENT_DELETED';
|
||||||
|
|
||||||
|
const newStatus = isPaid ? 'CONFIRMED' :
|
||||||
|
isOverdue ? 'OVERDUE' :
|
||||||
|
isRefunded ? 'REFUNDED' :
|
||||||
|
event === 'PAYMENT_RESTORED' ? 'PENDING' : null;
|
||||||
|
|
||||||
// If we have a matching or pending payment, let's update or insert
|
|
||||||
if (userPaymentIdx >= 0) {
|
if (userPaymentIdx >= 0) {
|
||||||
db.payments[userPaymentIdx].status =
|
if (newStatus) {
|
||||||
event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED' ? 'CONFIRMED' :
|
db.payments[userPaymentIdx].status = newStatus as any;
|
||||||
event === 'PAYMENT_OVERDUE' ? 'OVERDUE' :
|
}
|
||||||
event === 'PAYMENT_DELETED' ? 'DELETED' : db.payments[userPaymentIdx].status;
|
if (isPaid) {
|
||||||
|
|
||||||
if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') {
|
|
||||||
db.payments[userPaymentIdx].paidAt = new Date().toISOString();
|
db.payments[userPaymentIdx].paidAt = new Date().toISOString();
|
||||||
}
|
}
|
||||||
} else if (event !== 'PAYMENT_DELETED') {
|
} else if (!isRefunded) {
|
||||||
// Register a new payment log (unless it's a deletion of an unknown payment)
|
// Se não achou e não é estorno/deleção, registra a fatura enviada pelo Asaas
|
||||||
db.payments.push({
|
db.payments.push({
|
||||||
id: payment.id || `pay_wh_${Math.random().toString(36).substring(2, 9)}`,
|
id: payment.id, // Usa o ID exato
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
value: payment.value || 49.90,
|
value: payment.value || 49.90,
|
||||||
status: (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') ? 'CONFIRMED' :
|
status: (newStatus || 'PENDING') as any,
|
||||||
event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'PENDING',
|
|
||||||
billingType: payment.billingType || 'PIX',
|
billingType: payment.billingType || 'PIX',
|
||||||
invoiceUrl: payment.invoiceUrl || `https://sandbox.asaas.com/i/${payment.id}`,
|
invoiceUrl: payment.invoiceUrl || `https://sandbox.asaas.com/i/${payment.id}`,
|
||||||
dueDate: payment.dueDate || new Date().toISOString().split('T')[0],
|
dueDate: payment.dueDate || new Date().toISOString().split('T')[0],
|
||||||
paidAt: (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') ? new Date().toISOString() : null,
|
paidAt: isPaid ? new Date().toISOString() : null,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle subscription access control
|
// 4. Lógica de Liberação de Acesso / Curso
|
||||||
if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') {
|
const description = payment.description || '';
|
||||||
const description = payment.description || '';
|
const isCoursePayment = description.startsWith('unlock_course_');
|
||||||
|
|
||||||
if (description.startsWith('unlock_course_')) {
|
if (isPaid) {
|
||||||
|
if (isCoursePayment) {
|
||||||
const courseId = description.replace('unlock_course_', '');
|
const courseId = description.replace('unlock_course_', '');
|
||||||
if (!user.unlockedCourses) user.unlockedCourses = [];
|
if (!user.unlockedCourses) user.unlockedCourses = [];
|
||||||
if (!user.unlockedCourses.includes(courseId)) {
|
if (!user.unlockedCourses.includes(courseId)) {
|
||||||
|
|
@ -1198,16 +1209,16 @@ app.post('/api/webhooks/asaas', (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
console.log(`Webhook: Curso ${courseId} LIBERADO para usuário ${user.email}`);
|
console.log(`Webhook: Curso ${courseId} LIBERADO para usuário ${user.email}`);
|
||||||
} else {
|
} else {
|
||||||
// Liberar / manter acesso
|
|
||||||
user.subscriptionStatus = 'ACTIVE';
|
user.subscriptionStatus = 'ACTIVE';
|
||||||
console.log(`Webhook: Acesso LIBERADO para usuário ${user.email} (${user.name})`);
|
console.log(`Webhook: Assinatura LIBERADA para usuário ${user.email}`);
|
||||||
}
|
}
|
||||||
} else if (event === 'PAYMENT_OVERDUE' || event === 'SUBSCRIPTION_DELETED') {
|
} else if (isOverdue || isRefunded || event === 'SUBSCRIPTION_DELETED') {
|
||||||
// Bloquear acesso automaticamente se for assinatura
|
if (!isCoursePayment) {
|
||||||
const description = payment.description || '';
|
// Bloqueia acesso da assinatura
|
||||||
if (!description.startsWith('unlock_course_')) {
|
user.subscriptionStatus = isOverdue ? 'OVERDUE' : 'CANCELED';
|
||||||
user.subscriptionStatus = event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'CANCELED';
|
console.log(`Webhook: Assinatura BLOQUEADA para usuário ${user.email} - Motivo: ${event}`);
|
||||||
console.log(`Webhook: Acesso BLOQUEADO para usuário ${user.email} (${user.name}) - Motivo: ${event}`);
|
} else if (isRefunded) {
|
||||||
|
console.log(`Webhook: Pagamento de Curso REEMBOLSADO/DELETADO para usuário ${user.email}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue