Skip to content

Commit c39abfe

Browse files
committed
finalized qrCode payload, recreate checkin endpoint and service method, removed qrcode package (rendered in client)
1 parent 9bd2c66 commit c39abfe

File tree

6 files changed

+138
-315
lines changed

6 files changed

+138
-315
lines changed

lib/routes/guests.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
createGuest,
55
getGuests,
66
getGuest,
7-
getCurrentGuestTicketQrCode,
87
updateGuest,
98
archiveGuest
109
} from '../services/guests.js';
@@ -75,7 +74,7 @@ guestsRouter
7574
guestsRouter
7675
.get('/:id/ticket', async ctx => {
7776
try {
78-
const qrcode = await getCurrentGuestTicketQrCode(ctx.params.id);
77+
const qrcode = '';
7978

8079
if(!qrcode) throw ctx.throw(404);
8180

lib/routes/index.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -89,35 +89,35 @@ apiRouter
8989

9090
apiRouter
9191
.post('/check-ins', authorizeUser, requiresPermission('doorman'), async ctx => {
92-
// if(!ctx.request.body.ticketToken) throw ctx.throw(400);
93-
94-
// try {
95-
// const response = await checkInWithTicket(ctx.request.body.ticketToken, ctx.state.user.id);
96-
97-
// return ctx.body = response;
98-
// } catch(e) {
99-
// if(isRecordLike(e)) {
100-
// if(e.code === 'TICKET_NOT_FOUND') throw ctx.throw(404);
101-
102-
// // These codes will trigger a JSON response but 4xx status
103-
// const codeStatuses: Record<string, number> = {
104-
// 'GUEST_ALREADY_CHECKED_IN': 409,
105-
// 'EVENT_NOT_ACTIVE': 410,
106-
// 'EVENT_NOT_STARTED': 412,
107-
// 'TICKET_NOT_ACTIVE': 423,
108-
// 'GUEST_NOT_ACTIVE': 423
109-
// };
110-
// // For response bodies on errors, we need to manually set the response
111-
// // This will not trigger an error event, or stop upstream propagation
112-
// // if(Object.keys(codeStatuses).includes(e.code)) {
113-
// if(typeof e.code === 'string' && e.code in codeStatuses) {
114-
// ctx.status = codeStatuses[e.code];
115-
// return ctx.body = e.context;
116-
// }
117-
// }
118-
119-
// throw ctx.throw(e);
120-
// }
92+
if(!ctx.request.body.ticketToken) throw ctx.throw(400);
93+
94+
try {
95+
const response = await checkInWithTicket(ctx.request.body.ticketToken, ctx.state.user.id);
96+
97+
return ctx.body = response;
98+
} catch(e) {
99+
if(isRecordLike(e)) {
100+
if(e.code === 'TICKET_NOT_FOUND') throw ctx.throw(404);
101+
102+
// These codes will trigger a JSON response but 4xx status
103+
const codeStatuses: Record<string, number> = {
104+
'GUEST_ALREADY_CHECKED_IN': 409,
105+
'EVENT_NOT_ACTIVE': 410,
106+
'EVENT_NOT_STARTED': 412,
107+
'TICKET_NOT_ACTIVE': 423,
108+
'GUEST_NOT_ACTIVE': 423
109+
};
110+
// For response bodies on errors, we need to manually set the response
111+
// This will not trigger an error event, or stop upstream propagation
112+
// if(Object.keys(codeStatuses).includes(e.code)) {
113+
if(typeof e.code === 'string' && e.code in codeStatuses) {
114+
ctx.status = codeStatuses[e.code];
115+
return ctx.body = e.context;
116+
}
117+
}
118+
119+
throw ctx.throw(e);
120+
}
121121
});
122122

123123
apiRouter

lib/services/guests.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { v4 as uuidV4 } from 'uuid';
1010
import { sql } from '../utils/db.js';
1111

1212
class GuestsServiceError extends Error {
13-
constructor(message = 'An unknown error occured', code = 'UNKNOWN', context) {
13+
code: string;
14+
context?: unknown;
15+
16+
constructor(message = 'An unknown error occured', code = 'UNKNOWN', context?: unknown) {
1417
super(message);
1518

1619
this.name = this.constructor.name;
@@ -21,14 +24,6 @@ class GuestsServiceError extends Error {
2124
}
2225
}
2326

24-
const generateTicketToken = ({ id, created, ticketSeed }) => jwt.sign({
25-
iss: 'mustachebash',
26-
aud: 'ticket',
27-
iat: Math.round(created / 1000),
28-
sub: id
29-
},
30-
ticketSeed);
31-
3227
const guestColumns = [
3328
'id',
3429
'first_name',
@@ -112,22 +107,6 @@ export async function getGuest(id) {
112107
return guest;
113108
}
114109

115-
export async function getCurrentGuestTicketQrCode(guestId) {
116-
let ticket;
117-
try {
118-
[ ticket ] = await sql`
119-
SELECT *
120-
FROM tickets
121-
WHERE guest_id = ${guestId}
122-
AND status = 'active'
123-
`;
124-
} catch(e) {
125-
throw new GuestsServiceError('Could not query guest ticket', 'UNKNOWN', e);
126-
}
127-
128-
return generateQRDataURI(generateTicketToken(ticket));
129-
}
130-
131110
export async function updateGuest(id, updates) {
132111
for(const u in updates) {
133112
// Update whitelist

lib/services/tickets.ts

Lines changed: 91 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
* Handles all guest/ticket actions
44
* @type {Object}
55
*/
6-
import { toDataURL as generateQRDataURI } from 'qrcode';
7-
import jwt from 'jsonwebtoken';
86
import { v4 as uuidV4 } from 'uuid';
97
import { createGuest } from '../services/guests.js';
108
import log from '../utils/log.js';
119
import { sql } from '../utils/db.js';
1210

1311
class TicketsServiceError extends Error {
14-
constructor(message = 'An unknown error occured', code = 'UNKNOWN', context) {
12+
code: string;
13+
context?: unknown;
14+
15+
constructor(message = 'An unknown error occured', code = 'UNKNOWN', context?: unknown) {
1516
super(message);
1617

1718
this.name = this.constructor.name;
@@ -22,15 +23,15 @@ class TicketsServiceError extends Error {
2223
}
2324
}
2425

25-
// eslint-disable-next-line no-unused-vars
26-
const generateTicketToken = ({ id, created, ticketSeed }) => jwt.sign({
27-
aud: ticketSeed,
28-
iat: Math.round(created / 1000),
29-
sub: id
30-
},
31-
ticketSeed);
26+
// For now, ticket "seed" is fine to be used as plaintext since we can use it as
27+
// a revokable identifier, but are not rolling "live" tickets this year
28+
// ie, we aren't seeding a TOTP with it, and therefore it does not need to be a secret value.
29+
// This keeps the QR payload very short, and much quicker for scanning (both ease of reading and input time)
30+
function generateQRPayload(ticketSeed: string) {
31+
return ticketSeed;
32+
}
3233

33-
export async function getOrderTickets(orderId) {
34+
export async function getOrderTickets(orderId: string) {
3435
let guests;
3536
try {
3637
guests = await sql`
@@ -54,32 +55,32 @@ export async function getOrderTickets(orderId) {
5455
// Inject the QR Codes
5556
const tickets = [];
5657
for (const guest of guests) {
57-
// const qrCode = await generateQRDataURI(generateTicketToken(guest).split('.').pop());
58-
// eslint-disable-next-line no-unused-vars
59-
const qrCode = await generateQRDataURI(`${guest.id}:${Date.now()}`);
58+
const qrPayload = generateQRPayload(guest.ticketSeed);
6059

6160
tickets.push({
6261
id: guest.id,
6362
admissionTier: guest.admissionTier,
6463
eventId: guest.eventId,
6564
eventName: guest.eventName,
6665
eventDate: guest.eventDate,
67-
status: guest.status
68-
// qrCode
66+
status: guest.status,
67+
qrPayload
6968
});
7069
}
7170

7271
return tickets;
7372
}
7473

75-
export async function getCustomerActiveTicketsByOrderId(orderId) {
74+
export async function getCustomerActiveTicketsByOrderId(orderId: string) {
7675
let rows;
7776
try {
7877
rows = await sql`
7978
SELECT
8079
o.created AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles' as order_created,
8180
o.customer_id,
8281
g.id as guest_id,
82+
g.status as guest_status,
83+
g.check_in_time as guest_check_in_time,
8384
g.admission_tier as guest_admission_tier,
8485
g.ticket_seed as guest_ticket_seed,
8586
g.order_id as guest_order_id,
@@ -103,20 +104,20 @@ export async function getCustomerActiveTicketsByOrderId(orderId) {
103104
// Inject the QR Codes
104105
const tickets = [];
105106
for (const row of rows) {
106-
// const qrCode = await generateQRDataURI(generateTicketToken(guest).split('.').pop());
107-
// eslint-disable-next-line no-unused-vars
108-
const qrCode = await generateQRDataURI(`${row.guestId}:${Date.now()}`);
107+
const qrPayload = generateQRPayload(row.guestTicketSeed);
109108

110109
tickets.push({
111110
id: row.guestId,
112111
customerId: row.customerId,
113112
orderId: row.guestOrderId,
114113
orderCreated: row.orderCreated,
115114
admissionTier: row.guestAdmissionTier,
115+
status: row.guestStatus,
116+
checkInTime: row.guestCheckInTime,
116117
eventId: row.eventId,
117118
eventName: row.eventName,
118-
eventDate: row.eventDate
119-
// qrCode
119+
eventDate: row.eventDate,
120+
// qrPayload
120121
});
121122
}
122123

@@ -144,7 +145,20 @@ export async function getCustomerActiveTicketsByOrderId(orderId) {
144145
* are merely records of "guest" transfers
145146
* - The transferee will be upserted into customers, which means the original customer will need to input first/last/email
146147
*/
147-
export async function transferTickets(orderId, { transferee, guestIds }) {
148+
export async function transferTickets(
149+
orderId: string,
150+
{
151+
transferee,
152+
guestIds
153+
}: {
154+
transferee: {
155+
email: string;
156+
firstName: string;
157+
lastName: string;
158+
};
159+
guestIds: string[];
160+
}
161+
) {
148162
if(!transferee) throw new TicketsServiceError('No transferee specified', 'INVALID');
149163
if(!guestIds?.length) throw new TicketsServiceError('No tickets specified', 'INVALID');
150164

@@ -270,41 +284,58 @@ export async function transferTickets(orderId, { transferee, guestIds }) {
270284
};
271285
}
272286

273-
export function checkInWithTicket() {
274-
throw new TicketsServiceError('Not yet implemented', 'NOT_IMPLEMENTED');
275-
// let ticketId, ticketGuestId;
276-
// try {
277-
// ({ sub: ticketId, aud: ticketGuestId } = jwt.verify(ticketToken, config.jwt.ticketSecret, {issuer: 'mustachebash'}));
278-
// } catch(e) {
279-
// throw new TicketsServiceError('Invalid ticket token', 'INVALID_TICKET_TOKEN');
280-
// }
281-
282-
// const [ { ticket, guest, event } = {} ] = await run(r.table('tickets')
283-
// .getAll([ticketGuestId, ticketId ], {index: 'guestAndTicketId'})
284-
// .eqJoin('eventId', r.table('events'))
285-
// .map({
286-
// ticket: r.row('left'),
287-
// event: r.row('right'),
288-
// guest: r.table('guests').get(r.row('left')('guestId'))
289-
// }))
290-
// .then(cursor => cursor.toArray());
291-
292-
// if(!ticket) throw new TicketsServiceError('Ticket not found for guest', 'TICKET_NOT_FOUND');
293-
294-
// // All three entities must be active to check in
295-
// if(ticket.status !== 'active' && ticket.status !== 'consumed') throw new TicketsServiceError('Ticket no longer active', 'TICKET_NOT_ACTIVE', {ticket, guest, event});
296-
// if(guest.status !== 'active') throw new TicketsServiceError('Guest no longer active', 'GUEST_NOT_ACTIVE', {ticket, guest, event});
297-
// if(event.status !== 'active') throw new TicketsServiceError('Event no longer active', 'EVENT_NOT_ACTIVE', {ticket, guest, event});
298-
299-
// // Guests can't check in more than once
300-
// if(guest.checkedIn) throw new TicketsServiceError('Guest already checked in', 'GUEST_ALREADY_CHECKED_IN', {ticket, guest, event});
301-
302-
// // Guests can't check in before the event starts
303-
// if(event.enforceCheckInTime && new Date() < new Date(event.date)) throw new TicketsServiceError('Event has not started yet', 'EVENT_NOT_STARTED', {ticket, guest, event});
304-
305-
// // Ticket and check in is valid - mark guest as checked in and ticket as used (sequentially)
306-
// await run(r.table('guests').get(guest.id).update({checkedIn: r.now(), updated: r.now(), updatedBy: username}));
307-
// await run(r.table('tickets').get(ticket.id).update({status: 'consumed'}));
308-
309-
// return {event, guest, ticket};
287+
export async function checkInWithTicket(ticketToken: string, scannedBy: string) {
288+
let guest;
289+
// For now, this is happening directly with ticket seeds
290+
try {
291+
[guest] = await sql`
292+
SELECT
293+
g.id,
294+
g.first_name,
295+
g.last_name,
296+
g.status,
297+
g.order_id,
298+
g.admission_tier,
299+
g.check_in_time,
300+
e.id AS event_id,
301+
e.name AS event_name,
302+
e.date AS event_date,
303+
e.status AS event_status
304+
FROM guests AS g
305+
LEFT JOIN events AS e
306+
ON g.event_id = e.id
307+
WHERE g.ticket_seed = ${ticketToken}
308+
`;
309+
} catch(e) {
310+
throw new TicketsServiceError('Could not query guests for order', 'UNKNOWN', e);
311+
}
312+
313+
if(!guest) throw new TicketsServiceError('Ticket not found for guest', 'TICKET_NOT_FOUND');
314+
315+
// Guests can't check in more than once
316+
if(guest.status === 'checked_in') throw new TicketsServiceError('Guest already checked in', 'GUEST_ALREADY_CHECKED_IN', guest);
317+
// Both entities must be active to check in
318+
if(guest.status !== 'active') throw new TicketsServiceError('Guest no longer active', 'GUEST_NOT_ACTIVE', guest);
319+
if(guest.eventStatus !== 'active') throw new TicketsServiceError('Event no longer active', 'EVENT_NOT_ACTIVE', guest);
320+
321+
322+
// Guests can't check in earlier than 1 hour before the event starts
323+
if((new Date()).getTime() < (new Date(guest.eventDate)).getTime() - (1000 * 60 * 60 * 24 * 10)) throw new TicketsServiceError('Event has not started yet', 'EVENT_NOT_STARTED', guest);
324+
325+
// Ticket and check in is valid - mark guest as checked in
326+
try {
327+
await sql`
328+
UPDATE guests
329+
SET
330+
status = 'checked_in',
331+
check_in_time = now(),
332+
updated_by = ${scannedBy},
333+
updated = now()
334+
WHERE id = ${guest.id}
335+
`;
336+
} catch(e) {
337+
throw new TicketsServiceError('Could not update guest', 'UNKNOWN', e);
338+
}
339+
340+
return guest;
310341
}

0 commit comments

Comments
 (0)