Skip to content

Commit 11f28c7

Browse files
committed
Handle subscriptions
1 parent b55f99e commit 11f28c7

File tree

7 files changed

+1823
-13
lines changed

7 files changed

+1823
-13
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.db
2+
node_modules

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Subscription Backend
2+
3+
> Example of a simple backend for an app with user login and subscription management.
4+
5+
Using NodeJS and express.
6+
7+
### Usage
8+
9+
IAPTIC_PASSWORD=12345678-1234-1234-1234-1234567890
10+
ROUTE_PREFIX=/
11+
node index.js
12+
13+
### License
14+
15+
(c) 2023, Jean-Christophe Hoelt
16+
MIT License

app.js

+118-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,135 @@
11
const bodyParser = require('body-parser');
22
const express = require('express');
3-
const users = require('./users');
4-
3+
const cors = require('cors');
4+
const sessionsDB = require('./sessions-db');
5+
const subscriptionsDB = require('./subscriptions-db');
6+
57
const router = express.Router();
68
router.get('/', (req, res, next) => {
79
res.json({ok: true});
810
});
911

12+
/**
13+
* Opens a session for the user.
14+
*
15+
* Pass in the username as JSON in the body.
16+
* Password is not required, as this is just a demo.
17+
*/
1018
router.post('/login', (req, res, next) => {
11-
const token = users.userLogin(req.query['username']);
19+
const username = req.body.username;
20+
console.log('POST /login: ' + username);
21+
if (!username) return res.json({error: 'BadRequest'});
22+
const token = sessionsDB.login(username);
1223
res.json({token});
1324
});
1425

15-
router.get('/me/:token', (req, res, next) => {
16-
users.userSession(req.params.token, (err, user) => {
17-
res.json(err ? {error: err.message} : user);
18-
})
26+
/**
27+
* Load user information from a session token passed as a query parameter.
28+
*
29+
* User object is stored in `req.user`
30+
*/
31+
function fetchUser(req, res, next) {
32+
const token = req.query['token'];
33+
if (!token) return res.json({error: 'BadRequest'});
34+
sessionsDB.fromToken(token, (err, user) => {
35+
if (err)
36+
return res.json({error: err.message});
37+
subscriptionsDB.fetch(user.username, (err, subscription) => {
38+
if (err)
39+
return res.json({error: err.message});
40+
user.subscription = subscription;
41+
const expirationDate = user.subscription.expirationDate;
42+
if (expirationDate) {
43+
user.subscription.isExpired = expirationDate < new Date().toISOString();
44+
user.subscription.isActive = expirationDate > new Date().toISOString();
45+
}
46+
else {
47+
user.subscription.isActive = false;
48+
user.subscription.isExpired = false;
49+
}
50+
req.user = user;
51+
next();
52+
});
53+
});
54+
}
55+
56+
/**
57+
* Fetch information about the logged-in user.
58+
*
59+
* usage: GET /me?token=<value>
60+
*/
61+
router.get('/me', fetchUser, (req, res, next) => {
62+
console.log('GET /me: ' + req.user.username);
63+
res.json(req.user);
64+
});
65+
66+
/**
67+
* Handle webhook calls from iaptic.
68+
*
69+
* Store the subscription expiry date, and full information.
70+
*/
71+
router.post('/webhooks/iaptic', (req, res, next) => {
72+
const body = req.body;
73+
console.log('POST /webhooks/iaptic: ' + JSON.stringify(body, null, 2));
74+
if (body.password !== process.env.IAPTIC_PASSWORD) {
75+
res.status(401);
76+
res.json({ok: false});
77+
return;
78+
}
79+
switch (body.type) {
80+
case 'TEST':
81+
res.json({ok: true, result: 'TEST_PASSED'});
82+
break;
83+
case 'purchases.updated':
84+
const lastPurchase = Object.values(body.purchases).reduce((lastPurchase, purchase) => {
85+
if (!lastPurchase || purchase.expirationDate > lastPurchase.expirationDate) {
86+
return purchase;
87+
}
88+
return lastPurchase;
89+
}, undefined);
90+
subscriptionsDB.update(body.applicationUsername, lastPurchase);
91+
res.json({ok: true});
92+
break;
93+
default:
94+
res.json({ok: true, result: 'UNSUPPORTED'});
95+
break;
96+
}
97+
});
98+
99+
/**
100+
* Access public content.
101+
*
102+
* usage: GET /content/public/12345
103+
*/
104+
router.get('/content/public/:id', (req, res, next) => {
105+
console.log('GET /content/public/' + req.params.id);
106+
res.json({
107+
title: 'Free Content',
108+
content: 'This is some public content that everybody can access. 🍫',
109+
});
110+
});
111+
112+
/**
113+
* Access content reserved to subscribers.
114+
*
115+
* usage: GET /content/protected/12345?token=<value>
116+
*/
117+
router.get('/content/protected/:id', fetchUser, (req, res, next) => {
118+
console.log('GET /content/protected/' + req.params.id);
119+
const expirationDate = req.user.subscription ? req.user.subscription.expirationDate : undefined;
120+
if (!expirationDate || expirationDate < new Date().toISOString())
121+
return res.json({error: 'NoSubscription'});
122+
res.json({
123+
title: 'Premium Content',
124+
content: 'This is some information only subscribers can access. 💰',
125+
});
19126
});
20127

128+
const prefix = process.env.ROUTE_PREFIX || '/demo';
129+
console.log('Using prefix: ' + prefix);
21130
const app = express();
22131
app.use(bodyParser.json());
23132
app.use(bodyParser.urlencoded({ extended: false }));
24-
app.use('/', router);
133+
app.use(cors());
134+
app.use(process.env.ROUTE_PREFIX || '/demo', router);
25135
module.exports = app;

0 commit comments

Comments
 (0)