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');
});