Spaces:
Sleeping
Sleeping
File size: 4,975 Bytes
59b509f 072db80 59b509f fd68981 620611f 59b509f 7ccf1cc 59b509f b977bfa 59b509f 67d31ad 59b509f 7fe989f 6d4d78a 59b509f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
// paystack-webhook.js
// Node >= 14, install dependencies:
// npm install express firebase-admin
const express = require('express');
const crypto = require('crypto');
const admin = require('firebase-admin');
const app = express();
// -------------------------------------------------
// Environment variables (set these in your env/secrets)
// -------------------------------------------------
// - FIREBASE_CREDENTIALS: stringified JSON service account
// - PAYSTACK_SECRET: your Paystack webhook secret
// - PORT (optional)
// -------------------------------------------------
const {
FIREBASE_CREDENTIALS,
PAYSTACK_DEMO_SECRET,
PAYSTACK_LIVE_SECRET,
PORT,
} = process.env;
const PAYSTACK_SECRET = PAYSTACK_DEMO_SECRET; // PAYSTACK_LIVE_SECRET
if (!PAYSTACK_SECRET) {
console.warn('WARNING: PAYSTACK_SECRET is not set. Webhook signature verification will fail.');
}
// Initialize Firebase Admin using credentials read from env
if (FIREBASE_CREDENTIALS) {
try {
const serviceAccount = JSON.parse(FIREBASE_CREDENTIALS);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
console.log('Firebase admin initialized from FIREBASE_CREDENTIALS env var.');
} catch (err) {
console.error('Failed to parse FIREBASE_CREDENTIALS JSON:', err);
process.exit(1);
}
} else {
console.warn('FIREBASE_CREDENTIALS not provided. Firebase admin not initialized.');
}
// Important: use raw body parser to verify Paystack signature.
// Paystack signs the raw request body (HMAC SHA512) and puts signature in header `x-paystack-signature`
app.use(
express.raw({
type: 'application/json',
limit: '1mb',
})
);
// Utility: verify x-paystack-signature
function verifyPaystackSignature(rawBodyBuffer, signatureHeader) {
if (!PAYSTACK_SECRET) return false;
const hmac = crypto.createHmac('sha512', PAYSTACK_SECRET);
hmac.update(rawBodyBuffer);
const expected = hmac.digest('hex');
// Paystack header is hex string; compare in constant-time
return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(signatureHeader || '', 'utf8'));
}
// Helper: safe JSON parse
function safeParseJSON(raw) {
try {
return JSON.parse(raw);
} catch (err) {
return null;
}
}
// Webhook endpoint
app.post('/webhook/paystack', (req, res) => {
const raw = req.body; // Buffer because we used express.raw
const signature = req.get('x-paystack-signature') || req.get('X-Paystack-Signature');
// Verify signature
if (!signature || !verifyPaystackSignature(raw, signature)) {
console.warn('Paystack webhook signature verification failed. Signature header:', signature);
// Respond 400 to indicate invalid signature
return res.status(400).json({ ok: false, message: 'Invalid signature' });
}
// Parse payload
const bodyStr = raw.toString('utf8');
const payload = safeParseJSON(bodyStr);
if (!payload) {
console.warn('Could not parse webhook JSON payload.');
return res.status(400).json({ ok: false, message: 'Invalid JSON' });
}
// Paystack typically uses an "event" field: e.g. "charge.success", "refund.create", ...
const event = (payload.event || payload.type || '').toString();
// Identify refund events and payment events (first-time or recurring)
const isRefund = /refund/i.test(event); // matches refund.* etc.
const isChargeSuccess = /charge\.success/i.test(event); // common payment event
// add some broad matches for invoice/subscription events that might indicate recurring payments
const isInvoiceOrSubscription = /invoice\.(payment_succeeded|paid)|subscription\./i.test(event);
const isPayment = isChargeSuccess || isInvoiceOrSubscription;
// Only interested in refunds and payments
if (isRefund || isPayment) {
console.log('--- PAYSTACK WEBHOOK (INTERESTING EVENT) ---');
console.log('event:', event);
console.log('payload:', JSON.stringify(payload, null, 2));
// TODO: add your business logic here:
// - update Firestore / user subscription state
// - create refund records, notify user, retry flows, etc.
// Example (pseudo):
// const db = admin.firestore();
// await db.collection('paystack-webhooks').add({ receivedAt: admin.firestore.FieldValue.serverTimestamp(), event, payload });
// If you want to persist the webhook to Firestore for auditing, uncomment above and ensure Firebase is initialized
} else {
// For debugging, you can log minimal info for non-interesting events
console.log(`Received Paystack webhook for event "${event}" — ignored (not refund/payment).`);
}
// Acknowledge the webhook promptly with 200 OK so Paystack stops retrying
return res.status(200).json({ ok: true });
});
// Simple health check
app.get('/health', (_req, res) => res.json({ ok: true }));
app.listen(PORT, () => {
console.log(`Paystack webhook server listening on port ${PORT}`);
console.log('POST /webhook/paystack to receive Paystack callbacks');
}); |