Skip to content

Commit bf86727

Browse files
committed
Add webhook status polling endpoint
1 parent db5451b commit bf86727

File tree

5 files changed

+224
-6
lines changed

5 files changed

+224
-6
lines changed

app.js

+54-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const express = require('express');
33
const cors = require('cors');
44
const sessionsDB = require('./sessions-db');
55
const subscriptionsDB = require('./subscriptions-db');
6+
const verboseLogger = require('./verboseLogger');
7+
const webhooksDB = require('./webhooks-db');
68

79
const router = express.Router();
810
router.get('/', (req, res, next) => {
@@ -60,7 +62,20 @@ function fetchUser(req, res, next) {
6062
*/
6163
router.get('/me', fetchUser, (req, res, next) => {
6264
console.log('GET /me: ' + req.user.username);
63-
res.json(req.user);
65+
webhooksDB.getWebhookInfo(req.user.username, (err, webhookInfo) => {
66+
if (err) {
67+
console.error('Error getting webhook info:', err);
68+
webhookInfo = {};
69+
}
70+
req.user.webhookInfo = webhookInfo;
71+
const isWaiting = webhookInfo.wait_start_date && !webhookInfo.last_webhook_date || webhookInfo.last_webhook_date < webhookInfo.wait_start_date || webhookInfo.last_webhook_date > webhookInfo.wait_end_date;
72+
req.user.isWaitingForWebhook = isWaiting;
73+
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
74+
res.set('ETag', Date.now().toString());
75+
res.set('Last-Modified', new Date().toUTCString());
76+
res.status(200);
77+
res.json(req.user);
78+
});
6479
});
6580

6681
/**
@@ -72,30 +87,63 @@ router.post('/webhooks/iaptic', (req, res, next) => {
7287
const body = req.body;
7388
console.log('POST /webhooks/iaptic: ' + JSON.stringify(body, null, 2));
7489
if (body.password !== process.env.IAPTIC_PASSWORD) {
90+
console.log('Webhook request rejected: Invalid password');
7591
res.status(401);
76-
res.json({ok: false});
92+
res.json({ok: false, error: 'Invalid password'});
7793
return;
7894
}
7995
switch (body.type) {
8096
case 'TEST':
8197
res.json({ok: true, result: 'TEST_PASSED'});
8298
break;
8399
case 'purchases.updated':
100+
if (!body.applicationUsername) {
101+
console.log('Webhook request missing applicationUsername');
102+
res.status(200);
103+
res.json({ok: false, error: 'Missing applicationUsername'});
104+
return;
105+
}
106+
// Update the last webhook date for this user
107+
webhooksDB.updateWebhookInfo(body.applicationUsername, {
108+
lastWebhookDate: new Date().toISOString()
109+
});
84110
const lastPurchase = Object.values(body.purchases).reduce((lastPurchase, purchase) => {
85111
if (!lastPurchase || purchase.expirationDate > lastPurchase.expirationDate) {
86112
return purchase;
87113
}
88114
return lastPurchase;
89115
}, undefined);
116+
if (!lastPurchase) {
117+
console.log(`Removing subscription for user ${body.applicationUsername}`);
118+
subscriptionsDB.remove(body.applicationUsername, (err, wasRemoved) => {
119+
res.status(200);
120+
res.json({ok: true, result: wasRemoved ? 'REMOVED' : 'NO_SUBSCRIPTION'});
121+
});
122+
return;
123+
}
124+
console.log(`Updating subscription for user ${body.applicationUsername}`);
125+
console.log(`Last purchase details: ${JSON.stringify(lastPurchase, null, 2)}`);
90126
subscriptionsDB.update(body.applicationUsername, lastPurchase);
127+
91128
res.json({ok: true});
92129
break;
93130
default:
131+
console.log(`Webhook request rejected: Unsupported type ${body.type}`);
94132
res.json({ok: true, result: 'UNSUPPORTED'});
95133
break;
96134
}
97135
});
98136

137+
// Add this new endpoint for initiating webhook wait
138+
router.post('/pending-webhooks', fetchUser, (req, res, next) => {
139+
const username = req.user.username;
140+
const waitStartDate = new Date(Date.now() - 10 * 1000).toISOString(); // Assume the webbook might have been sent a few seconds ago already
141+
const waitEndDate = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // Wait for 1 hour
142+
143+
webhooksDB.updateWebhookInfo(username, { waitStartDate, waitEndDate });
144+
res.json({ok: true, message: 'Webhook wait initiated'});
145+
});
146+
99147
/**
100148
* Access public content.
101149
*
@@ -119,6 +167,7 @@ router.get('/content/protected/:id', fetchUser, (req, res, next) => {
119167
const expirationDate = req.user.subscription ? req.user.subscription.expirationDate : undefined;
120168
if (!expirationDate || expirationDate < new Date().toISOString())
121169
return res.json({error: 'NoSubscription'});
170+
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
122171
res.json({
123172
title: 'Premium Content',
124173
content: 'This is some information only subscribers can access. 💰',
@@ -131,5 +180,6 @@ const app = express();
131180
app.use(bodyParser.json());
132181
app.use(bodyParser.urlencoded({ extended: false }));
133182
app.use(cors());
134-
app.use(process.env.ROUTE_PREFIX || '/demo', router);
135-
module.exports = app;
183+
app.use(verboseLogger); // Add verbose logger middleware
184+
app.use(prefix, router);
185+
module.exports = app;

index.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
const http = require('http');
44
const app = require('./app');
55

6+
// Add a verbose mode flag
7+
const verboseMode = process.env.VERBOSE_MODE === 'true';
8+
69
const port = parseInt(process.env.PORT || '8000');
7-
// app.set('port', port);
810
const server = http.createServer(app);
911
server.on('error', onError);
1012
server.on('listening', onListening);
@@ -42,5 +44,23 @@ function onListening() {
4244
? 'pipe ' + addr
4345
: 'port ' + addr.port;
4446
console.log('Listening on ' + bind);
47+
48+
// Add verbose logging
49+
if (verboseMode) {
50+
console.log('Verbose mode enabled');
51+
console.log('Server configuration:');
52+
console.log(`- Port: ${port}`);
53+
console.log(`- Route prefix: ${process.env.ROUTE_PREFIX || '/demo'}`);
54+
console.log(`- Iaptic password set: ${!!process.env.IAPTIC_PASSWORD}`);
55+
console.log('Environment variables:');
56+
Object.keys(process.env).forEach(key => {
57+
if (key !== 'IAPTIC_PASSWORD') { // Don't log sensitive information
58+
console.log(`- ${key}: ${process.env[key]}`);
59+
}
60+
});
61+
}
4562
}
4663

64+
// Export the verboseLogger for use in app.js
65+
module.exports = { server };
66+

subscriptions-db.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,17 @@ function fetch(username, callback) {
5050
});
5151
}
5252

53-
module.exports = {update, fetch}
53+
function remove(username, callback) {
54+
db.run('DELETE FROM subscriptions WHERE username = ?', [username], function(err) {
55+
if (err) {
56+
console.error('Error attempting to delete subscription:', err);
57+
callback(err);
58+
return;
59+
}
60+
const wasDeleted = this.changes > 0;
61+
console.log(`Subscription for user ${username} ${wasDeleted ? 'deleted successfully' : 'not found or already deleted'}.`);
62+
callback(null, wasDeleted);
63+
});
64+
}
65+
66+
module.exports = {update, fetch, remove}

verboseLogger.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const verboseMode = process.env.VERBOSE_MODE === 'true';
2+
3+
function verboseLogger(req, res, next) {
4+
if (verboseMode) {
5+
const timestamp = new Date().toISOString();
6+
const method = req.method;
7+
const url = req.url;
8+
const ip = req.ip || req.connection.remoteAddress;
9+
const userAgent = req.get('User-Agent');
10+
11+
console.log(`[${timestamp}] ${method} ${url}`);
12+
console.log(` IP: ${ip}`);
13+
console.log(` User-Agent: ${userAgent}`);
14+
15+
if (Object.keys(req.query).length > 0) {
16+
console.log(' Query params:', req.query);
17+
}
18+
19+
if (req.body && Object.keys(req.body).length > 0) {
20+
console.log(' Body:', JSON.stringify(req.body, null, 2));
21+
}
22+
23+
// Capture the original json method
24+
const originalJson = res.json;
25+
26+
// Override the json method
27+
res.json = function(body) {
28+
// Log the response body
29+
console.log(' Response:', JSON.stringify(body, null, 2));
30+
31+
// Call the original json method
32+
originalJson.call(this, body);
33+
};
34+
35+
// Log when the response is finished
36+
res.on('finish', () => {
37+
console.log(` Status: ${res.statusCode}`);
38+
console.log('---');
39+
});
40+
}
41+
next();
42+
}
43+
44+
module.exports = verboseLogger;

webhooks-db.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* WebhooksDB to track webhook information for users.
3+
*/
4+
5+
const sqlite3 = require('sqlite3');
6+
const db = new sqlite3.Database('webhooks.db');
7+
8+
db.run(`CREATE TABLE IF NOT EXISTS webhooks (
9+
username TEXT PRIMARY KEY,
10+
last_webhook_date TEXT,
11+
wait_start_date TEXT,
12+
wait_end_date TEXT
13+
)`);
14+
15+
/**
16+
* Update webhook information for a user
17+
* @param {string} username - The user's identifier
18+
* @param {Object} data - Object containing webhook data
19+
* @param {string} [data.lastWebhookDate] - ISO8601 formatted date string of the last received webhook
20+
* @param {string} [data.waitStartDate] - ISO8601 formatted date string for when to start waiting
21+
* @param {string} [data.waitEndDate] - ISO8601 formatted date string for when to stop waiting
22+
*/
23+
function updateWebhookInfo(username, data) {
24+
const { lastWebhookDate, waitStartDate, waitEndDate } = data;
25+
db.run(`INSERT OR REPLACE INTO webhooks
26+
(username, last_webhook_date, wait_start_date, wait_end_date)
27+
VALUES (?,
28+
COALESCE(?, (SELECT last_webhook_date FROM webhooks WHERE username = ?)),
29+
COALESCE(?, (SELECT wait_start_date FROM webhooks WHERE username = ?)),
30+
COALESCE(?, (SELECT wait_end_date FROM webhooks WHERE username = ?))
31+
)`,
32+
username,
33+
lastWebhookDate, username,
34+
waitStartDate, username,
35+
waitEndDate, username
36+
);
37+
}
38+
39+
/**
40+
* Get webhook information for a user
41+
* @param {string} username - The user's identifier
42+
* @param {function} callback - Callback function(err, webhookInfo)
43+
*/
44+
function getWebhookInfo(username, callback) {
45+
db.get('SELECT * FROM webhooks WHERE username = ?',
46+
[username],
47+
(err, row) => {
48+
if (err) {
49+
callback(err, null);
50+
} else {
51+
callback(null, row || {});
52+
}
53+
}
54+
);
55+
}
56+
57+
/**
58+
* Check if a user is waiting for a webhook
59+
* @param {string} username - The user's identifier
60+
* @param {function} callback - Callback function(err, isWaiting)
61+
*
62+
function isWaitingForWebhook(username, callback) {
63+
const now = new Date().toISOString();
64+
db.get('SELECT * FROM webhooks WHERE username = ? AND wait_start_date <= ? AND wait_end_date > ?',
65+
[username, now, now],
66+
(err, row) => {
67+
if (err) {
68+
callback(err, false);
69+
} else {
70+
callback(null, !!row);
71+
}
72+
}
73+
);
74+
}
75+
*/
76+
77+
/**
78+
* Clean up expired webhook waits
79+
*/
80+
function cleanupExpiredWaits() {
81+
const now = new Date().toISOString();
82+
db.run('UPDATE webhooks SET wait_start_date = NULL, wait_end_date = NULL WHERE wait_end_date <= ?', now);
83+
}
84+
85+
// Run cleanup periodically (e.g., every hour)
86+
setInterval(cleanupExpiredWaits, 60 * 60 * 1000);
87+
88+
module.exports = {
89+
updateWebhookInfo,
90+
getWebhookInfo
91+
};

0 commit comments

Comments
 (0)