Skip to content

Commit 5cca5a9

Browse files
committed
Started basic public PDF endpoint to view/print tickets
1 parent 9ae10c8 commit 5cca5a9

9 files changed

+1094
-9
lines changed

lib/config.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
},
1111
jwt: {
1212
secret: process.env.JWT_SECRET,
13+
transactionSecret: process.env.JWT_TRANSACTION_SECRET,
1314
ticketSecret: process.env.JWT_TICKET_SECRET
1415
},
1516
mailgun: {

lib/routes/index.js

+29
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
const api = require('express').Router(),
66
{ authorizeUser } = require('../middleware/auth'),
77
{ authenticateUser, refreshAccessToken } = require('../services/auth'),
8+
{ validateTransactionToken } = require('../services/transactions'),
9+
{ generateTicketsPDF } = require('../services/pdf'),
10+
{ getTransactionTickets } = require('../services/guests'),
811
transactionsRouter = require('./transactions'),
912
sitesRouter = require('./sites'),
1013
eventsRouter = require('./events'),
@@ -21,6 +24,32 @@ api.use('/promos', promosRouter);
2124
api.use('/guests', authorizeUser, guestsRouter);
2225
api.use('/users', authorizeUser, usersRouter);
2326

27+
api.route('/mytickets')
28+
.get(async (req, res, next) => {
29+
if(!req.query.t) return next(400);
30+
31+
let transactionId;
32+
try {
33+
({ sub: transactionId } = validateTransactionToken(req.query.t));
34+
} catch(e) {
35+
next(e);
36+
}
37+
38+
let tickets;
39+
try {
40+
tickets = await getTransactionTickets(transactionId);
41+
} catch(e) {
42+
if (e.code === 'UNAUTHORIZED') return next(401);
43+
44+
next(e);
45+
}
46+
47+
const ticketsPDF = generateTicketsPDF(tickets);
48+
49+
ticketsPDF.pipe(res);
50+
ticketsPDF.end();
51+
});
52+
2453
api.route('/authenticate')
2554
.post(async (req, res, next) => {
2655
if(!req.body.username || !req.body.password) return next(400);

lib/routes/transactions.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const transactionsRouter = require('express').Router(),
22
{ authorizeUser } = require('../middleware/auth'),
3-
{ createTransaction, getTransactions, getTransaction } = require('../services/transactions'),
3+
{ createTransaction, getTransactions, getTransaction, generateTransactionToken } = require('../services/transactions'),
44
{ sendReceipt, upsertEmailSubscriber } = require('../services/email');
55

66
// TODO: make this configurable at some point
@@ -55,4 +55,17 @@ transactionsRouter.route('/:id')
5555
}
5656
});
5757

58+
transactionsRouter.route('/:id/token')
59+
.get(authorizeUser, async (req, res, next) => {
60+
try {
61+
const transactionToken = await generateTransactionToken(req.params.id);
62+
63+
res.json(transactionToken);
64+
} catch(e) {
65+
if(e.code === 'NOT_FOUND') return next(404);
66+
67+
next(e);
68+
}
69+
});
70+
5871
module.exports = transactionsRouter;

lib/services/guests.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class GuestsServiceError extends Error {
1919
}
2020
}
2121

22-
const createTicketToken = ({ id, guestId, created }) => jwt.sign({
22+
const generateTicketToken = ({ id, guestId, created }) => jwt.sign({
2323
iss: 'mustachebash',
2424
aud: guestId,
2525
iat: Math.round(created / 1000),
@@ -84,7 +84,7 @@ module.exports = {
8484
async getTicketQrCode(guestId, ticketId) {
8585
const [ ticket ] = await run(r.table('tickets').getAll([guestId, ticketId], {index: 'guestAndTicketId'})).then(cursor => cursor.toArray());
8686

87-
return generateQRDataURI(JSON.stringify(createTicketToken(ticket)));
87+
return generateQRDataURI(JSON.stringify(generateTicketToken(ticket)));
8888
},
8989

9090
async createGuestTicket(guestId, { createdBy = 'purchase' } = {}) {
@@ -111,13 +111,30 @@ module.exports = {
111111
async getCurrentGuestTicketQrCode(guestId) {
112112
const [ ticket ] = await run(r.table('tickets').getAll(guestId, {index: 'guestId'}).filter({status: 'active'})).then(cursor => cursor.toArray());
113113

114-
return generateQRDataURI(JSON.stringify(createTicketToken(ticket)));
114+
return generateQRDataURI(JSON.stringify(generateTicketToken(ticket)));
115115
},
116116

117117
getGuestTickets(guestId) {
118118
return run(r.table('tickets').getAll(guestId, {index: 'guestId'})).then(cursor => cursor.toArray());
119119
},
120120

121+
async getTransactionTickets(transactionId) {
122+
const query = r.table('guests')
123+
.filter({transactionId})
124+
.eqJoin('id', r.table('tickets'), {index: 'guestId'})
125+
.filter({right: {status: 'active'}})
126+
.map({guest: r.row('left'), ticket: r.row('right')});
127+
128+
const pairs = await run(query).then(cursor => cursor.toArray());
129+
130+
// Inject the QR Codes
131+
for (const pair of pairs) {
132+
pair.ticket.qrCode = await generateQRDataURI(JSON.stringify(generateTicketToken(pair.ticket)));
133+
}
134+
135+
return pairs;
136+
},
137+
121138
async updateGuest(id, updates) {
122139
for(const u in updates) {
123140
// Update whitelist

lib/services/pdf.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* PDF service for generating downloadable tickets
3+
* @type {Object}
4+
*/
5+
6+
const PDFDocument = require('pdfkit');
7+
8+
module.exports = {
9+
generateTicketsPDF(guestsTicketsPairs) {
10+
const doc = new PDFDocument({margin: 50, autoFirstPage: false});
11+
12+
guestsTicketsPairs.forEach(({ guest, ticket }) => {
13+
doc.addPage();
14+
15+
const { firstName, lastName } = guest,
16+
{ qrCode } = ticket;
17+
18+
doc.fontSize(20)
19+
.text('Mustache Bash 2020 - Ticket', 0, 57, {align: 'center'})
20+
.fontSize(14)
21+
.text(`for ${firstName} ${lastName}`, 0, 97, {align: 'center'})
22+
.image(qrCode, 0, 200);
23+
});
24+
25+
return doc;
26+
}
27+
};

lib/services/transactions.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* @type {object}
44
*/
55
const braintree = require('braintree'),
6+
jwt = require('jsonwebtoken'),
67
{ run, r } = require('../utils/db'),
78
{ createGuest, createGuestTicket } = require('../services/guests'),
8-
{ braintree: btConfig, donationProductId } = require('../config');
9+
{ braintree: btConfig, donationProductId, jwt: { transactionSecret } } = require('../config');
910

1011
const gateway = braintree.connect({
1112
environment: braintree.Environment[btConfig.environment],
@@ -204,6 +205,24 @@ module.exports = {
204205
return changes[0].new_val;
205206
},
206207

208+
async generateTransactionToken(id) {
209+
const transaction = await run(r.table('transactions').get(id));
210+
211+
if(!transaction) throw new TransactionsServiceError('Transaction not found', 'NOT_FOUND');
212+
213+
return jwt.sign({
214+
iss: 'mustachebash',
215+
aud: 'tickets',
216+
iat: Math.round(transaction.created / 1000),
217+
sub: id
218+
},
219+
transactionSecret);
220+
},
221+
222+
validateTransactionToken(token) {
223+
return jwt.verify(token, transactionSecret, {issuer: 'mustachebash'});
224+
},
225+
207226
getTransaction(id) {
208227
return run(r.table('transactions').get(id));
209228
}

0 commit comments

Comments
 (0)