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
93
server.ts
93
server.ts
|
|
@ -1127,70 +1127,81 @@ app.post('/api/admin/reorder', authenticateToken, requireAdmin, (req: Authentica
|
|||
// --- ASAAS WEBHOOK HANDLER ---
|
||||
app.post('/api/webhooks/asaas', (req: Request, res: Response) => {
|
||||
const db = loadDb();
|
||||
const configuredSecret = db.settings?.asaasWebhookSecret || process.env.ASAAS_WEBHOOK_TOKEN || 'devflix_webhook_secret_key';
|
||||
const { event, payment } = req.body;
|
||||
|
||||
// 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'];
|
||||
|
||||
const tokenHeader = req.headers['asaas-token'] || req.headers['authorization'];
|
||||
if (configuredSecret && tokenHeader !== configuredSecret) {
|
||||
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.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { event, payment } = req.body;
|
||||
|
||||
if (!event || !payment) {
|
||||
res.status(400).json({ error: 'Formato do Webhook inválido' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the webhook event
|
||||
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
|
||||
// 3. Processamento do Pagamento
|
||||
const customerId = payment.customer;
|
||||
const user = db.users.find(u => u.asaasCustomerId === customerId);
|
||||
|
||||
if (user) {
|
||||
// Process payment updates
|
||||
let userPaymentIdx = db.payments.findIndex(p => p.id === payment.id || (p.userId === user.id && p.status === 'PENDING'));
|
||||
// Busca APENAS pelo ID exato da fatura
|
||||
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) {
|
||||
db.payments[userPaymentIdx].status =
|
||||
event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED' ? 'CONFIRMED' :
|
||||
event === 'PAYMENT_OVERDUE' ? 'OVERDUE' :
|
||||
event === 'PAYMENT_DELETED' ? 'DELETED' : db.payments[userPaymentIdx].status;
|
||||
|
||||
if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') {
|
||||
if (newStatus) {
|
||||
db.payments[userPaymentIdx].status = newStatus as any;
|
||||
}
|
||||
if (isPaid) {
|
||||
db.payments[userPaymentIdx].paidAt = new Date().toISOString();
|
||||
}
|
||||
} else if (event !== 'PAYMENT_DELETED') {
|
||||
// Register a new payment log (unless it's a deletion of an unknown payment)
|
||||
} else if (!isRefunded) {
|
||||
// Se não achou e não é estorno/deleção, registra a fatura enviada pelo Asaas
|
||||
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,
|
||||
value: payment.value || 49.90,
|
||||
status: (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') ? 'CONFIRMED' :
|
||||
event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'PENDING',
|
||||
status: (newStatus || 'PENDING') as any,
|
||||
billingType: payment.billingType || 'PIX',
|
||||
invoiceUrl: payment.invoiceUrl || `https://sandbox.asaas.com/i/${payment.id}`,
|
||||
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()
|
||||
});
|
||||
}
|
||||
|
||||
// Handle subscription access control
|
||||
if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') {
|
||||
const description = payment.description || '';
|
||||
// 4. Lógica de Liberação de Acesso / Curso
|
||||
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_', '');
|
||||
if (!user.unlockedCourses) user.unlockedCourses = [];
|
||||
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}`);
|
||||
} else {
|
||||
// Liberar / manter acesso
|
||||
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') {
|
||||
// Bloquear acesso automaticamente se for assinatura
|
||||
const description = payment.description || '';
|
||||
if (!description.startsWith('unlock_course_')) {
|
||||
user.subscriptionStatus = event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'CANCELED';
|
||||
console.log(`Webhook: Acesso BLOQUEADO para usuário ${user.email} (${user.name}) - Motivo: ${event}`);
|
||||
} else if (isOverdue || isRefunded || event === 'SUBSCRIPTION_DELETED') {
|
||||
if (!isCoursePayment) {
|
||||
// Bloqueia acesso da assinatura
|
||||
user.subscriptionStatus = isOverdue ? 'OVERDUE' : 'CANCELED';
|
||||
console.log(`Webhook: Assinatura BLOQUEADA para usuário ${user.email} - Motivo: ${event}`);
|
||||
} else if (isRefunded) {
|
||||
console.log(`Webhook: Pagamento de Curso REEMBOLSADO/DELETADO para usuário ${user.email}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue