Pepguy commited on
Commit
77cc094
·
verified ·
1 Parent(s): 204eaf6

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +88 -28
app.js CHANGED
@@ -1,10 +1,11 @@
1
  // paystack-webhook.js
2
  // Node >= 14, install dependencies:
3
- // npm install express firebase-admin
4
 
5
  const express = require('express');
6
  const crypto = require('crypto');
7
  const admin = require('firebase-admin');
 
8
 
9
  const app = express();
10
 
@@ -12,7 +13,8 @@ const app = express();
12
  // Environment variables (set these in your env/secrets)
13
  // -------------------------------------------------
14
  // - FIREBASE_CREDENTIALS: stringified JSON service account
15
- // - PAYSTACK_SECRET: your Paystack webhook secret
 
16
  // - PORT (optional)
17
  // -------------------------------------------------
18
 
@@ -20,13 +22,14 @@ const {
20
  FIREBASE_CREDENTIALS,
21
  PAYSTACK_DEMO_SECRET,
22
  PAYSTACK_LIVE_SECRET,
23
- PORT,
24
  } = process.env;
25
 
26
- const PAYSTACK_SECRET = PAYSTACK_DEMO_SECRET; // PAYSTACK_LIVE_SECRET
 
27
 
28
  if (!PAYSTACK_SECRET) {
29
- console.warn('WARNING: PAYSTACK_SECRET is not set. Webhook signature verification will fail.');
30
  }
31
 
32
  // Initialize Firebase Admin using credentials read from env
@@ -45,8 +48,9 @@ if (FIREBASE_CREDENTIALS) {
45
  console.warn('FIREBASE_CREDENTIALS not provided. Firebase admin not initialized.');
46
  }
47
 
48
- // Important: use raw body parser to verify Paystack signature.
49
- // Paystack signs the raw request body (HMAC SHA512) and puts signature in header `x-paystack-signature`
 
50
  app.use(
51
  express.raw({
52
  type: 'application/json',
@@ -60,8 +64,11 @@ function verifyPaystackSignature(rawBodyBuffer, signatureHeader) {
60
  const hmac = crypto.createHmac('sha512', PAYSTACK_SECRET);
61
  hmac.update(rawBodyBuffer);
62
  const expected = hmac.digest('hex');
63
- // Paystack header is hex string; compare in constant-time
64
- return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(signatureHeader || '', 'utf8'));
 
 
 
65
  }
66
 
67
  // Helper: safe JSON parse
@@ -73,7 +80,9 @@ function safeParseJSON(raw) {
73
  }
74
  }
75
 
76
- // Webhook endpoint
 
 
77
  app.post('/webhook/paystack', (req, res) => {
78
  const raw = req.body; // Buffer because we used express.raw
79
  const signature = req.get('x-paystack-signature') || req.get('X-Paystack-Signature');
@@ -81,7 +90,6 @@ app.post('/webhook/paystack', (req, res) => {
81
  // Verify signature
82
  if (!signature || !verifyPaystackSignature(raw, signature)) {
83
  console.warn('Paystack webhook signature verification failed. Signature header:', signature);
84
- // Respond 400 to indicate invalid signature
85
  return res.status(400).json({ ok: false, message: 'Invalid signature' });
86
  }
87
 
@@ -94,44 +102,96 @@ app.post('/webhook/paystack', (req, res) => {
94
  return res.status(400).json({ ok: false, message: 'Invalid JSON' });
95
  }
96
 
97
- // Paystack typically uses an "event" field: e.g. "charge.success", "refund.create", ...
98
  const event = (payload.event || payload.type || '').toString();
99
-
100
- // Identify refund events and payment events (first-time or recurring)
101
- const isRefund = /refund/i.test(event); // matches refund.* etc.
102
- const isChargeSuccess = /charge\.success/i.test(event); // common payment event
103
- // add some broad matches for invoice/subscription events that might indicate recurring payments
104
  const isInvoiceOrSubscription = /invoice\.(payment_succeeded|paid)|subscription\./i.test(event);
105
-
106
  const isPayment = isChargeSuccess || isInvoiceOrSubscription;
107
 
108
- // Only interested in refunds and payments
109
  if (isRefund || isPayment) {
110
  console.log('--- PAYSTACK WEBHOOK (INTERESTING EVENT) ---');
111
  console.log('event:', event);
112
  console.log('payload:', JSON.stringify(payload, null, 2));
113
 
114
- // TODO: add your business logic here:
115
- // - update Firestore / user subscription state
116
- // - create refund records, notify user, retry flows, etc.
117
- // Example (pseudo):
118
  // const db = admin.firestore();
119
- // await db.collection('paystack-webhooks').add({ receivedAt: admin.firestore.FieldValue.serverTimestamp(), event, payload });
120
-
121
- // If you want to persist the webhook to Firestore for auditing, uncomment above and ensure Firebase is initialized
 
 
122
  } else {
123
- // For debugging, you can log minimal info for non-interesting events
124
  console.log(`Received Paystack webhook for event "${event}" — ignored (not refund/payment).`);
125
  }
126
 
127
- // Acknowledge the webhook promptly with 200 OK so Paystack stops retrying
128
  return res.status(200).json({ ok: true });
129
  });
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  // Simple health check
132
  app.get('/health', (_req, res) => res.json({ ok: true }));
133
 
134
  app.listen(PORT, () => {
135
  console.log(`Paystack webhook server listening on port ${PORT}`);
136
  console.log('POST /webhook/paystack to receive Paystack callbacks');
 
137
  });
 
1
  // paystack-webhook.js
2
  // Node >= 14, install dependencies:
3
+ // npm install express firebase-admin axios
4
 
5
  const express = require('express');
6
  const crypto = require('crypto');
7
  const admin = require('firebase-admin');
8
+ const axios = require('axios');
9
 
10
  const app = express();
11
 
 
13
  // Environment variables (set these in your env/secrets)
14
  // -------------------------------------------------
15
  // - FIREBASE_CREDENTIALS: stringified JSON service account
16
+ // - PAYSTACK_DEMO_SECRET
17
+ // - PAYSTACK_LIVE_SECRET
18
  // - PORT (optional)
19
  // -------------------------------------------------
20
 
 
22
  FIREBASE_CREDENTIALS,
23
  PAYSTACK_DEMO_SECRET,
24
  PAYSTACK_LIVE_SECRET,
25
+ PORT = 3000,
26
  } = process.env;
27
 
28
+ // choose which secret to use (dev/demo by default)
29
+ const PAYSTACK_SECRET = PAYSTACK_DEMO_SECRET || PAYSTACK_LIVE_SECRET;
30
 
31
  if (!PAYSTACK_SECRET) {
32
+ console.warn('WARNING: PAYSTACK_SECRET is not set. Outgoing Paystack calls and webhook verification will fail.');
33
  }
34
 
35
  // Initialize Firebase Admin using credentials read from env
 
48
  console.warn('FIREBASE_CREDENTIALS not provided. Firebase admin not initialized.');
49
  }
50
 
51
+ // -------------------------------------------------
52
+ // Webhook raw parser (required to verify Paystack signature)
53
+ // -------------------------------------------------
54
  app.use(
55
  express.raw({
56
  type: 'application/json',
 
64
  const hmac = crypto.createHmac('sha512', PAYSTACK_SECRET);
65
  hmac.update(rawBodyBuffer);
66
  const expected = hmac.digest('hex');
67
+ try {
68
+ return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(signatureHeader || '', 'utf8'));
69
+ } catch (e) {
70
+ return false;
71
+ }
72
  }
73
 
74
  // Helper: safe JSON parse
 
80
  }
81
  }
82
 
83
+ // ------------------------
84
+ // Existing webhook endpoint
85
+ // ------------------------
86
  app.post('/webhook/paystack', (req, res) => {
87
  const raw = req.body; // Buffer because we used express.raw
88
  const signature = req.get('x-paystack-signature') || req.get('X-Paystack-Signature');
 
90
  // Verify signature
91
  if (!signature || !verifyPaystackSignature(raw, signature)) {
92
  console.warn('Paystack webhook signature verification failed. Signature header:', signature);
 
93
  return res.status(400).json({ ok: false, message: 'Invalid signature' });
94
  }
95
 
 
102
  return res.status(400).json({ ok: false, message: 'Invalid JSON' });
103
  }
104
 
 
105
  const event = (payload.event || payload.type || '').toString();
106
+ const isRefund = /refund/i.test(event);
107
+ const isChargeSuccess = /charge\.success/i.test(event);
 
 
 
108
  const isInvoiceOrSubscription = /invoice\.(payment_succeeded|paid)|subscription\./i.test(event);
 
109
  const isPayment = isChargeSuccess || isInvoiceOrSubscription;
110
 
 
111
  if (isRefund || isPayment) {
112
  console.log('--- PAYSTACK WEBHOOK (INTERESTING EVENT) ---');
113
  console.log('event:', event);
114
  console.log('payload:', JSON.stringify(payload, null, 2));
115
 
116
+ // Example: persist webhook to Firestore (uncomment if you want)
 
 
 
117
  // const db = admin.firestore();
118
+ // db.collection('paystack-webhooks').add({
119
+ // event,
120
+ // payload,
121
+ // receivedAt: admin.firestore.FieldValue.serverTimestamp(),
122
+ // });
123
  } else {
 
124
  console.log(`Received Paystack webhook for event "${event}" — ignored (not refund/payment).`);
125
  }
126
 
 
127
  return res.status(200).json({ ok: true });
128
  });
129
 
130
+ // ------------------------
131
+ // New: Create Payment Page (link) for a plan
132
+ // ------------------------
133
+ // This endpoint expects JSON body, so we attach express.json() for this route.
134
+ // Required fields in body: planId, userId, name (friendly page name) optionally: amount, redirect_url, collect_phone, fixed_amount
135
+ app.post('/create-payment-link', express.json(), async (req, res) => {
136
+ const { planId, userId, name, amount, redirect_url, collect_phone = false, fixed_amount = false } = req.body || {};
137
+
138
+ if (!planId) return res.status(400).json({ ok: false, message: 'planId is required' });
139
+ if (!userId) return res.status(400).json({ ok: false, message: 'userId is required' });
140
+ if (!name) return res.status(400).json({ ok: false, message: 'name is required (page title)' });
141
+
142
+ // Build the body per Paystack's Payment Pages API.
143
+ // We'll set type to "subscription" (so it links to a plan) and pass metadata.userId
144
+ // Also add a custom_fields array so the dashboard shows the user id if you open the transaction.
145
+ // See: Paystack Payment Pages and Metadata docs. 1
146
+ const payload = {
147
+ name,
148
+ type: 'subscription',
149
+ plan: planId,
150
+ metadata: {
151
+ userId,
152
+ },
153
+ custom_fields: [
154
+ {
155
+ display_name: 'User ID',
156
+ variable_name: 'user_id',
157
+ value: userId,
158
+ },
159
+ ],
160
+ // optional properties
161
+ collect_phone,
162
+ fixed_amount,
163
+ };
164
+
165
+ if (amount) payload.amount = amount; // amount is optional (in kobo/cents)
166
+ if (redirect_url) payload.redirect_url = redirect_url;
167
+
168
+ try {
169
+ const response = await axios.post('https://api.paystack.co/page', payload, {
170
+ headers: {
171
+ Authorization: `Bearer ${PAYSTACK_SECRET}`,
172
+ 'Content-Type': 'application/json',
173
+ },
174
+ timeout: 10_000,
175
+ });
176
+
177
+ // Paystack returns a response with data object. Return minimal info to caller.
178
+ const pageData = response.data && response.data.data ? response.data.data : response.data;
179
+
180
+ // The created page usually has a slug; you can build public url with https://paystack.com/pay/[slug]
181
+ // Return the page data so caller can redirect or store details.
182
+ return res.status(201).json({ ok: true, page: pageData });
183
+ } catch (err) {
184
+ console.error('Error creating Paystack payment page:', err?.response?.data || err.message || err);
185
+ const errorDetail = err?.response?.data || { message: err.message };
186
+ return res.status(500).json({ ok: false, error: errorDetail });
187
+ }
188
+ });
189
+
190
  // Simple health check
191
  app.get('/health', (_req, res) => res.json({ ok: true }));
192
 
193
  app.listen(PORT, () => {
194
  console.log(`Paystack webhook server listening on port ${PORT}`);
195
  console.log('POST /webhook/paystack to receive Paystack callbacks');
196
+ console.log('POST /create-payment-link to create a subscription payment page (expects JSON body).');
197
  });