@unchainedshop/ticketing 4.3.3 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/lib/express.d.ts +0 -1
- package/lib/express.js +3 -4
- package/lib/fastify.d.ts +0 -1
- package/lib/fastify.js +3 -4
- package/lib/index.d.ts +6 -7
- package/lib/index.js +4 -5
- package/lib/magic-key.d.ts +0 -1
- package/lib/magic-key.js +0 -4
- package/lib/mobile-tickets/apple-handler-express.d.ts +1 -2
- package/lib/mobile-tickets/apple-handler-express.js +1 -8
- package/lib/mobile-tickets/apple-handler-fastify.d.ts +0 -1
- package/lib/mobile-tickets/apple-handler-fastify.js +1 -8
- package/lib/mobile-tickets/apple-wallet.d.ts +0 -1
- package/lib/mobile-tickets/apple-wallet.js +0 -1
- package/lib/mobile-tickets/google-handler-express.d.ts +1 -2
- package/lib/mobile-tickets/google-handler-express.js +0 -1
- package/lib/mobile-tickets/google-handler-fastify.d.ts +0 -1
- package/lib/mobile-tickets/google-handler-fastify.js +0 -1
- package/lib/module.d.ts +4 -5
- package/lib/module.js +3 -10
- package/lib/pdf-tickets/print-handler-express.d.ts +1 -2
- package/lib/pdf-tickets/print-handler-express.js +1 -2
- package/lib/pdf-tickets/print-handler-fastify.d.ts +0 -1
- package/lib/pdf-tickets/print-handler-fastify.js +1 -2
- package/lib/services.d.ts +2 -3
- package/lib/services.js +0 -1
- package/lib/template-registry.d.ts +10 -10
- package/lib/template-registry.js +5 -7
- package/lib/types.d.ts +3 -4
- package/lib/types.js +0 -1
- package/package.json +17 -15
- package/lib/express.d.ts.map +0 -1
- package/lib/express.js.map +0 -1
- package/lib/fastify.d.ts.map +0 -1
- package/lib/fastify.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/magic-key.d.ts.map +0 -1
- package/lib/magic-key.js.map +0 -1
- package/lib/mobile-tickets/apple-handler-express.d.ts.map +0 -1
- package/lib/mobile-tickets/apple-handler-express.js.map +0 -1
- package/lib/mobile-tickets/apple-handler-fastify.d.ts.map +0 -1
- package/lib/mobile-tickets/apple-handler-fastify.js.map +0 -1
- package/lib/mobile-tickets/apple-wallet.d.ts.map +0 -1
- package/lib/mobile-tickets/apple-wallet.js.map +0 -1
- package/lib/mobile-tickets/apple-webservice.d.ts +0 -7
- package/lib/mobile-tickets/apple-webservice.d.ts.map +0 -1
- package/lib/mobile-tickets/apple-webservice.js +0 -177
- package/lib/mobile-tickets/apple-webservice.js.map +0 -1
- package/lib/mobile-tickets/google-handler-express.d.ts.map +0 -1
- package/lib/mobile-tickets/google-handler-express.js.map +0 -1
- package/lib/mobile-tickets/google-handler-fastify.d.ts.map +0 -1
- package/lib/mobile-tickets/google-handler-fastify.js.map +0 -1
- package/lib/mobile-tickets/google-webservice.d.ts +0 -7
- package/lib/mobile-tickets/google-webservice.d.ts.map +0 -1
- package/lib/mobile-tickets/google-webservice.js +0 -52
- package/lib/mobile-tickets/google-webservice.js.map +0 -1
- package/lib/module.d.ts.map +0 -1
- package/lib/module.js.map +0 -1
- package/lib/pdf-tickets/print-handler-express.d.ts.map +0 -1
- package/lib/pdf-tickets/print-handler-express.js.map +0 -1
- package/lib/pdf-tickets/print-handler-fastify.d.ts.map +0 -1
- package/lib/pdf-tickets/print-handler-fastify.js.map +0 -1
- package/lib/pdf-tickets/print-webservice.d.ts +0 -7
- package/lib/pdf-tickets/print-webservice.d.ts.map +0 -1
- package/lib/pdf-tickets/print-webservice.js +0 -31
- package/lib/pdf-tickets/print-webservice.js.map +0 -1
- package/lib/services.d.ts.map +0 -1
- package/lib/services.js.map +0 -1
- package/lib/template-registry.d.ts.map +0 -1
- package/lib/template-registry.js.map +0 -1
- package/lib/ticketing-index.d.ts +0 -19
- package/lib/ticketing-index.d.ts.map +0 -1
- package/lib/ticketing-index.js +0 -40
- package/lib/ticketing-index.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/readme.md +0 -118
- package/src/express.ts +0 -35
- package/src/fastify.ts +0 -30
- package/src/index.ts +0 -66
- package/src/magic-key.ts +0 -43
- package/src/mobile-tickets/apple-handler-express.ts +0 -237
- package/src/mobile-tickets/apple-handler-fastify.ts +0 -279
- package/src/mobile-tickets/apple-wallet.ts +0 -31
- package/src/mobile-tickets/google-handler-express.ts +0 -57
- package/src/mobile-tickets/google-handler-fastify.ts +0 -69
- package/src/module.ts +0 -251
- package/src/pdf-tickets/print-handler-express.ts +0 -34
- package/src/pdf-tickets/print-handler-fastify.ts +0 -43
- package/src/services.ts +0 -35
- package/src/template-registry.ts +0 -48
- package/src/types.ts +0 -8
- package/tsconfig.json +0 -10
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '@unchainedshop/logger';
|
|
2
|
-
import { TicketingAPI } from '../types.js';
|
|
3
|
-
import { getFileAdapter } from '@unchainedshop/core-files';
|
|
4
|
-
import type { FastifyRequest, RouteHandlerMethod } from 'fastify';
|
|
5
|
-
|
|
6
|
-
const logger = createLogger('unchained:apple-wallet-webservice');
|
|
7
|
-
|
|
8
|
-
const isAuthenticationTokenCorrect = (req, authenticationToken) => {
|
|
9
|
-
const expectedAuthorizationValue = `ApplePass ${authenticationToken}`;
|
|
10
|
-
return req.headers.authorization === expectedAuthorizationValue;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const appleWalletHandler: RouteHandlerMethod = async (
|
|
14
|
-
req: FastifyRequest & {
|
|
15
|
-
unchainedContext: TicketingAPI;
|
|
16
|
-
},
|
|
17
|
-
reply,
|
|
18
|
-
) => {
|
|
19
|
-
const resolvedContext = req.unchainedContext;
|
|
20
|
-
const { modules } = resolvedContext;
|
|
21
|
-
|
|
22
|
-
const path = req.url;
|
|
23
|
-
|
|
24
|
-
logger.info(`${path} (${JSON.stringify(req.query)})`);
|
|
25
|
-
|
|
26
|
-
if (path.startsWith('/download/')) {
|
|
27
|
-
try {
|
|
28
|
-
const [, , passFileName] = path.split('/');
|
|
29
|
-
const [tokenId] = passFileName.split('.pkpass');
|
|
30
|
-
|
|
31
|
-
if (!tokenId) {
|
|
32
|
-
reply.status(404);
|
|
33
|
-
return reply.send();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const token = await modules.warehousing.findToken({
|
|
37
|
-
tokenId,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!token) {
|
|
41
|
-
logger.error('Token not found', { tokenId });
|
|
42
|
-
reply.status(404);
|
|
43
|
-
return reply.send();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const { hash } = req.query as Record<string, string>;
|
|
47
|
-
const correctHash = await modules.warehousing.buildAccessKeyForToken(tokenId);
|
|
48
|
-
if (!hash || hash !== correctHash) {
|
|
49
|
-
logger.error('Token hash invalid for current owner', { tokenId });
|
|
50
|
-
reply.status(403);
|
|
51
|
-
return reply.send({
|
|
52
|
-
success: false,
|
|
53
|
-
message: 'Token hash invalid for current owner',
|
|
54
|
-
name: 'HASH_MISMATCH',
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const passFile = await modules.passes.upsertAppleWalletPass(token, resolvedContext);
|
|
59
|
-
|
|
60
|
-
const fileUploadAdapter = getFileAdapter();
|
|
61
|
-
const signedUrl = await fileUploadAdapter.createDownloadURL(passFile);
|
|
62
|
-
const url = signedUrl && (await modules.files.normalizeUrl(signedUrl, {}));
|
|
63
|
-
|
|
64
|
-
if (!url) {
|
|
65
|
-
reply.status(500);
|
|
66
|
-
return reply.send({
|
|
67
|
-
success: false,
|
|
68
|
-
message: 'Could not create download URL',
|
|
69
|
-
name: 'URL_SIGNING_FAILED',
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const response = await fetch(url);
|
|
74
|
-
const data = await response.arrayBuffer();
|
|
75
|
-
const uint8View = new Uint8Array(data);
|
|
76
|
-
|
|
77
|
-
reply.status(200);
|
|
78
|
-
reply.header('content-type', 'application/vnd.apple.pkpass');
|
|
79
|
-
reply.header('Content-Disposition', `attachment; filename=${tokenId}.pkpass`);
|
|
80
|
-
return reply.send(uint8View);
|
|
81
|
-
} catch (e) {
|
|
82
|
-
logger.error(e);
|
|
83
|
-
reply.status(500);
|
|
84
|
-
return reply.send({
|
|
85
|
-
success: false,
|
|
86
|
-
message: 'Error generating pass',
|
|
87
|
-
name: 'PASS_GENERATION_ERROR',
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const [, apiVersion, endpoint, ...pathComponents] = path.split("/"); /* eslint-disable-line */
|
|
93
|
-
if (endpoint === 'devices') {
|
|
94
|
-
if (req.method === 'POST') {
|
|
95
|
-
// Register Device
|
|
96
|
-
const [deviceLibraryIdentifier, , passTypeIdentifier, serialNumber] = pathComponents;
|
|
97
|
-
const { pushToken } = req.body as Record<string, any>;
|
|
98
|
-
|
|
99
|
-
const pass = await modules.passes.findAppleWalletPass(passTypeIdentifier, serialNumber);
|
|
100
|
-
|
|
101
|
-
if (pass) {
|
|
102
|
-
if (
|
|
103
|
-
!isAuthenticationTokenCorrect(
|
|
104
|
-
req,
|
|
105
|
-
(pass.meta?.rawData as any)?._id || (pass.meta?.rawData as any)?.tokenId,
|
|
106
|
-
)
|
|
107
|
-
) {
|
|
108
|
-
logger.error('Unauthorized', { passTypeIdentifier, serialNumber });
|
|
109
|
-
reply.status(401);
|
|
110
|
-
return reply.send();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const newRegistration = await modules.passes.registerDeviceForAppleWalletPass(
|
|
114
|
-
passTypeIdentifier,
|
|
115
|
-
serialNumber,
|
|
116
|
-
{
|
|
117
|
-
deviceLibraryIdentifier,
|
|
118
|
-
pushToken,
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (!newRegistration) {
|
|
123
|
-
reply.status(200);
|
|
124
|
-
return reply.send({
|
|
125
|
-
success: true,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
reply.status(201);
|
|
130
|
-
return reply.send({
|
|
131
|
-
success: true,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
} else if (req.method === 'GET') {
|
|
135
|
-
// Get the List of Updatable Passes
|
|
136
|
-
const lastUpdated = new Date();
|
|
137
|
-
const [deviceLibraryIdentifier, , passTypeIdentifier] = pathComponents;
|
|
138
|
-
const passesUpdatedSince =
|
|
139
|
-
(req.query as Record<string, string>)?.passesUpdatedSince &&
|
|
140
|
-
new Date((req.query as Record<string, string>).passesUpdatedSince as string);
|
|
141
|
-
|
|
142
|
-
const passes = await modules.passes.findUpdatedAppleWalletPasses(
|
|
143
|
-
passTypeIdentifier,
|
|
144
|
-
deviceLibraryIdentifier,
|
|
145
|
-
passesUpdatedSince,
|
|
146
|
-
);
|
|
147
|
-
const serialNumbers = passes.map((t) => t.meta?.serialNumber).filter(Boolean) as string[];
|
|
148
|
-
|
|
149
|
-
if (serialNumbers?.length) {
|
|
150
|
-
reply.status(200);
|
|
151
|
-
return reply.send({
|
|
152
|
-
success: true,
|
|
153
|
-
serialNumbers,
|
|
154
|
-
lastUpdated,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
reply.status(204);
|
|
159
|
-
return reply.send({ success: true });
|
|
160
|
-
} else if (req.method === 'DELETE') {
|
|
161
|
-
// Unregister Device
|
|
162
|
-
const [deviceLibraryIdentifier, , passTypeIdentifier, serialNumber] = pathComponents;
|
|
163
|
-
|
|
164
|
-
const pass = await modules.passes.findAppleWalletPass(passTypeIdentifier, serialNumber);
|
|
165
|
-
|
|
166
|
-
if (pass) {
|
|
167
|
-
if (
|
|
168
|
-
!isAuthenticationTokenCorrect(
|
|
169
|
-
req,
|
|
170
|
-
(pass.meta?.rawData as any)?._id || (pass.meta?.rawData as any)?.tokenId,
|
|
171
|
-
)
|
|
172
|
-
) {
|
|
173
|
-
logger.error('Unauthorized', { passTypeIdentifier, serialNumber });
|
|
174
|
-
reply.status(401);
|
|
175
|
-
return reply.send({
|
|
176
|
-
success: false,
|
|
177
|
-
message: 'Unauthorized',
|
|
178
|
-
name: 'UNAUTHORIZED',
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
await modules.passes.unregisterDeviceForAppleWalletPass(
|
|
183
|
-
passTypeIdentifier,
|
|
184
|
-
serialNumber,
|
|
185
|
-
deviceLibraryIdentifier,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// Unregistered
|
|
189
|
-
reply.status(200);
|
|
190
|
-
return reply.send({
|
|
191
|
-
success: true,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} else if (endpoint === 'log') {
|
|
196
|
-
// Log Mesage
|
|
197
|
-
const { logs } = req.body as Record<string, any>;
|
|
198
|
-
logs?.forEach((log) => {
|
|
199
|
-
if (typeof log === 'string') {
|
|
200
|
-
logger.info(log);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
reply.status(200);
|
|
204
|
-
return reply.send({
|
|
205
|
-
success: true,
|
|
206
|
-
});
|
|
207
|
-
} else if (endpoint === 'passes') {
|
|
208
|
-
if (req.method === 'GET') {
|
|
209
|
-
// Get an updated Pass
|
|
210
|
-
const [passTypeIdentifier, serialNumber] = pathComponents;
|
|
211
|
-
|
|
212
|
-
const pass = await modules.passes.findAppleWalletPass(passTypeIdentifier, serialNumber);
|
|
213
|
-
|
|
214
|
-
if (!pass) {
|
|
215
|
-
reply.status(404);
|
|
216
|
-
return reply.send();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
!isAuthenticationTokenCorrect(
|
|
221
|
-
req,
|
|
222
|
-
(pass.meta?.rawData as any)?._id || (pass.meta?.rawData as any)?.tokenId,
|
|
223
|
-
)
|
|
224
|
-
) {
|
|
225
|
-
logger.error('Unauthorized', { passTypeIdentifier, serialNumber });
|
|
226
|
-
reply.status(401);
|
|
227
|
-
return reply.send({
|
|
228
|
-
success: false,
|
|
229
|
-
message: 'Unauthorized',
|
|
230
|
-
name: 'UNAUTHORIZED',
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const { updated, created } = pass;
|
|
235
|
-
|
|
236
|
-
const lastModifiedDate = new Date(updated || created);
|
|
237
|
-
lastModifiedDate.setMilliseconds(0);
|
|
238
|
-
|
|
239
|
-
const ifModifiedSinceDate = new Date(req.headers['if-modified-since']!);
|
|
240
|
-
ifModifiedSinceDate.setMilliseconds(0);
|
|
241
|
-
|
|
242
|
-
if (ifModifiedSinceDate.getTime() >= lastModifiedDate.getTime()) {
|
|
243
|
-
reply.status(304);
|
|
244
|
-
return reply.send({
|
|
245
|
-
success: true,
|
|
246
|
-
message: 'Not modified',
|
|
247
|
-
name: 'NOT_MODIFIED',
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const fileUploadAdapter = getFileAdapter();
|
|
252
|
-
const signedUrl = await fileUploadAdapter.createDownloadURL(pass);
|
|
253
|
-
const url = signedUrl && (await modules.files.normalizeUrl(signedUrl, {}));
|
|
254
|
-
|
|
255
|
-
if (!url) {
|
|
256
|
-
reply.status(500);
|
|
257
|
-
return reply.send({
|
|
258
|
-
success: false,
|
|
259
|
-
message: 'Could not create download URL',
|
|
260
|
-
name: 'URL_SIGNING_FAILED',
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const result = await fetch(url);
|
|
265
|
-
const data = await result.arrayBuffer();
|
|
266
|
-
const uint8View = new Uint8Array(data);
|
|
267
|
-
|
|
268
|
-
reply.status(200);
|
|
269
|
-
reply.header('content-type', 'application/vnd.apple.pkpass');
|
|
270
|
-
reply.header('last-modified', lastModifiedDate.toUTCString());
|
|
271
|
-
return reply.send(uint8View);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
reply.status(404);
|
|
276
|
-
return reply.send();
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
export default appleWalletHandler;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import apn from '@parse/node-apn';
|
|
2
|
-
import { Readable } from 'node:stream';
|
|
3
|
-
|
|
4
|
-
export const pushToApplePushNotificationService = async (deviceTokens) => {
|
|
5
|
-
const apnProvider = new apn.Provider({
|
|
6
|
-
cert: process.env.PASS_CERTIFICATE_PATH,
|
|
7
|
-
key: process.env.PASS_CERTIFICATE_PATH,
|
|
8
|
-
passphrase: process.env.PASS_CERTIFICATE_SECRET,
|
|
9
|
-
production: true,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const note = new apn.Notification({});
|
|
13
|
-
return apnProvider.send(note, deviceTokens);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const buildPassBinary = async (
|
|
17
|
-
tokenSerialNumber: string,
|
|
18
|
-
pass: {
|
|
19
|
-
serialNumber: string;
|
|
20
|
-
asBuffer: () => Promise<Buffer>;
|
|
21
|
-
},
|
|
22
|
-
) => {
|
|
23
|
-
const passBuffer = await pass.asBuffer();
|
|
24
|
-
const rawFile = {
|
|
25
|
-
_id: pass.serialNumber,
|
|
26
|
-
filename: `${tokenSerialNumber}-${new Date().getTime()}.pkpass`,
|
|
27
|
-
createReadStream: () => Readable.from(passBuffer),
|
|
28
|
-
mimetype: 'application/vnd.apple.pkpass',
|
|
29
|
-
};
|
|
30
|
-
return rawFile;
|
|
31
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '@unchainedshop/logger';
|
|
2
|
-
import type { Request, Response } from 'express';
|
|
3
|
-
import { TicketingAPI } from '../types.js';
|
|
4
|
-
|
|
5
|
-
const logger = createLogger('unchained:google-wallet-webservice');
|
|
6
|
-
|
|
7
|
-
export const googleWalletHandler = async (
|
|
8
|
-
req: Request & { unchainedContext: TicketingAPI },
|
|
9
|
-
res: Response,
|
|
10
|
-
) => {
|
|
11
|
-
const resolvedContext = req.unchainedContext;
|
|
12
|
-
const { modules } = resolvedContext;
|
|
13
|
-
logger.info(`${req.path} (${JSON.stringify(req.query)})`);
|
|
14
|
-
|
|
15
|
-
if (req.path.includes('/download/')) {
|
|
16
|
-
try {
|
|
17
|
-
const { tokenId } = req.params as { tokenId: string };
|
|
18
|
-
const { hash } = req.query as { hash?: string };
|
|
19
|
-
|
|
20
|
-
if (!tokenId) {
|
|
21
|
-
res.status(404).end();
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const token = await modules.warehousing.findToken({
|
|
26
|
-
tokenId,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (!token) {
|
|
30
|
-
res.status(404).send('Token not found');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const correctHash = await modules.warehousing.buildAccessKeyForToken(tokenId);
|
|
35
|
-
if (!hash || hash !== correctHash) {
|
|
36
|
-
res.status(403).send('Token hash invalid for current owner');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const pass = await modules.passes.upsertGoogleWalletPass(token, resolvedContext);
|
|
41
|
-
|
|
42
|
-
if (!pass) {
|
|
43
|
-
throw new Error('Could not create Google Wallet pass');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
res.redirect(await pass.asURL());
|
|
47
|
-
return;
|
|
48
|
-
} catch (e) {
|
|
49
|
-
logger.error(e);
|
|
50
|
-
}
|
|
51
|
-
res.status(500).end();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
res.status(404).end();
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export default googleWalletHandler;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '@unchainedshop/logger';
|
|
2
|
-
import { TicketingAPI } from '../types.js';
|
|
3
|
-
import type { FastifyRequest, RouteHandlerMethod } from 'fastify';
|
|
4
|
-
|
|
5
|
-
const logger = createLogger('unchained:google-wallet-webservice');
|
|
6
|
-
|
|
7
|
-
const googleWalletHandler: RouteHandlerMethod = async (
|
|
8
|
-
req: FastifyRequest & {
|
|
9
|
-
unchainedContext: TicketingAPI;
|
|
10
|
-
},
|
|
11
|
-
reply,
|
|
12
|
-
) => {
|
|
13
|
-
const resolvedContext = req.unchainedContext;
|
|
14
|
-
const { modules } = resolvedContext;
|
|
15
|
-
|
|
16
|
-
const path = req.url;
|
|
17
|
-
|
|
18
|
-
logger.info(`${path} (${JSON.stringify(req.query)})`);
|
|
19
|
-
if (path.includes('/download/')) {
|
|
20
|
-
try {
|
|
21
|
-
const { tokenId } = req.params as { tokenId: string };
|
|
22
|
-
const { hash } = req.query as { hash?: string };
|
|
23
|
-
if (!tokenId) {
|
|
24
|
-
reply.status(404);
|
|
25
|
-
return reply.send();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const token = await modules.warehousing.findToken({
|
|
29
|
-
tokenId,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (!token) {
|
|
33
|
-
logger.error('Token not found', { tokenId });
|
|
34
|
-
reply.status(404);
|
|
35
|
-
return reply.send('Token not found');
|
|
36
|
-
}
|
|
37
|
-
const correctHash = await modules.warehousing.buildAccessKeyForToken(tokenId);
|
|
38
|
-
if (!hash || hash !== correctHash) {
|
|
39
|
-
logger.error('Token hash invalid for current owner', { tokenId });
|
|
40
|
-
reply.status(403);
|
|
41
|
-
return reply.send({
|
|
42
|
-
success: false,
|
|
43
|
-
message: 'Token hash invalid for current owner',
|
|
44
|
-
name: 'HASH_MISMATCH',
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const pass = await modules.passes.upsertGoogleWalletPass(token, resolvedContext);
|
|
49
|
-
|
|
50
|
-
if (!pass) {
|
|
51
|
-
throw new Error('Could not create Google Wallet pass');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return reply.redirect(await pass.asURL());
|
|
55
|
-
} catch (e) {
|
|
56
|
-
logger.error(e);
|
|
57
|
-
reply.status(500);
|
|
58
|
-
return reply.send({
|
|
59
|
-
success: false,
|
|
60
|
-
message: 'Error generating pass',
|
|
61
|
-
name: 'PASS_GENERATION_ERROR',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
reply.status(404);
|
|
66
|
-
return reply.send();
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export default googleWalletHandler;
|
package/src/module.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '@unchainedshop/logger';
|
|
2
|
-
import { buildDbIndexes, ModuleInput } from '@unchainedshop/mongodb';
|
|
3
|
-
import { MediaObjectsCollection } from '@unchainedshop/core-files';
|
|
4
|
-
import { TokenSurrogateCollection } from '@unchainedshop/core-warehousing';
|
|
5
|
-
import { UnchainedCore } from '@unchainedshop/core';
|
|
6
|
-
import { TokenSurrogate } from '@unchainedshop/core-warehousing';
|
|
7
|
-
import { File } from '@unchainedshop/core-files';
|
|
8
|
-
|
|
9
|
-
import { RendererTypes, getRenderer } from './template-registry.js';
|
|
10
|
-
import { buildPassBinary, pushToApplePushNotificationService } from './mobile-tickets/apple-wallet.js';
|
|
11
|
-
|
|
12
|
-
export const APPLE_WALLET_PASSES_FILE_DIRECTORY = 'apple-wallet-passes';
|
|
13
|
-
|
|
14
|
-
const logger = createLogger('unchained:apple-wallet-webservice');
|
|
15
|
-
|
|
16
|
-
const configurePasses = async ({ db }: ModuleInput<Record<string, never>>) => {
|
|
17
|
-
const MediaObjects = await MediaObjectsCollection(db);
|
|
18
|
-
const TokenSurrogates = await TokenSurrogateCollection(db);
|
|
19
|
-
|
|
20
|
-
await buildDbIndexes(TokenSurrogates as any, [
|
|
21
|
-
{ index: { 'meta.cancelled': 1 }, options: { sparse: true } },
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
await buildDbIndexes(MediaObjects as any, [
|
|
25
|
-
{ index: { path: 1, 'meta.passTypeIdentifier': 1, 'meta.serialNumber': 1 } },
|
|
26
|
-
{
|
|
27
|
-
index: {
|
|
28
|
-
path: 1,
|
|
29
|
-
'meta.passTypeIdentifier': 1,
|
|
30
|
-
'meta.registrations.deviceLibraryIdentifier': 1,
|
|
31
|
-
},
|
|
32
|
-
} as any,
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
const upsertAppleWalletPass = async (token: TokenSurrogate, unchainedAPI: UnchainedCore) => {
|
|
36
|
-
const createAppleWalletPass = getRenderer(RendererTypes.APPLE_WALLET);
|
|
37
|
-
const pass = await createAppleWalletPass(token, unchainedAPI);
|
|
38
|
-
const rawFile = Promise.resolve(
|
|
39
|
-
// wrap in promise to make stream upload work
|
|
40
|
-
await buildPassBinary(token.tokenSerialNumber, pass as any),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const previousFile = await MediaObjects.findOne({
|
|
44
|
-
path: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
45
|
-
'meta.passTypeIdentifier': pass.passTypeIdentifier,
|
|
46
|
-
'meta.serialNumber': pass.serialNumber,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const registrations: any = previousFile?.meta?.registrations || [];
|
|
50
|
-
const pkpassFile = await unchainedAPI.services.files.uploadFileFromStream({
|
|
51
|
-
directoryName: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
52
|
-
rawFile,
|
|
53
|
-
meta: {
|
|
54
|
-
rawData: token,
|
|
55
|
-
passTypeIdentifier: pass.passTypeIdentifier,
|
|
56
|
-
serialNumber: pass.serialNumber,
|
|
57
|
-
registrations,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
if (previousFile) {
|
|
62
|
-
await unchainedAPI.services.files.removeFiles({
|
|
63
|
-
fileIds: [previousFile._id],
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Push updates!
|
|
68
|
-
if (registrations?.length) {
|
|
69
|
-
try {
|
|
70
|
-
const pushTokens = registrations.map(({ pushToken }) => pushToken);
|
|
71
|
-
logger.info(`Send update pass notification to ${pushTokens.join(',')}`);
|
|
72
|
-
await pushToApplePushNotificationService(pushTokens);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
logger.error(e);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return pkpassFile;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const upsertGoogleWalletPass = async (token: TokenSurrogate, unchainedAPI: UnchainedCore) => {
|
|
82
|
-
const createGoogleWalletPass = getRenderer(RendererTypes.GOOGLE_WALLET);
|
|
83
|
-
const pass = await createGoogleWalletPass(token, unchainedAPI);
|
|
84
|
-
return pass;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const findAppleWalletPass = async (passTypeIdentifier, serialNumber) => {
|
|
88
|
-
const mediaObject = await MediaObjects.findOne({
|
|
89
|
-
path: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
90
|
-
'meta.passTypeIdentifier': passTypeIdentifier,
|
|
91
|
-
'meta.serialNumber': serialNumber,
|
|
92
|
-
});
|
|
93
|
-
return mediaObject;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const registerDeviceForAppleWalletPass = async (
|
|
97
|
-
passTypeIdentifier,
|
|
98
|
-
serialNumber,
|
|
99
|
-
{ deviceLibraryIdentifier, pushToken },
|
|
100
|
-
) => {
|
|
101
|
-
const result = await MediaObjects.updateOne(
|
|
102
|
-
{
|
|
103
|
-
path: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
104
|
-
'meta.passTypeIdentifier': passTypeIdentifier,
|
|
105
|
-
'meta.serialNumber': serialNumber,
|
|
106
|
-
'meta.registrations.deviceLibraryIdentifier': {
|
|
107
|
-
$ne: deviceLibraryIdentifier,
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
$push: {
|
|
112
|
-
'meta.registrations': {
|
|
113
|
-
deviceLibraryIdentifier,
|
|
114
|
-
pushToken,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
);
|
|
119
|
-
return Boolean(result.modifiedCount);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const unregisterDeviceForAppleWalletPass = async (
|
|
123
|
-
passTypeIdentifier,
|
|
124
|
-
serialNumber,
|
|
125
|
-
deviceLibraryIdentifier,
|
|
126
|
-
) => {
|
|
127
|
-
const result = await MediaObjects.updateOne(
|
|
128
|
-
{
|
|
129
|
-
path: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
130
|
-
'meta.passTypeIdentifier': passTypeIdentifier,
|
|
131
|
-
'meta.serialNumber': serialNumber,
|
|
132
|
-
'meta.registrations.deviceLibraryIdentifier': deviceLibraryIdentifier,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
$pull: {
|
|
136
|
-
'meta.registrations': {
|
|
137
|
-
deviceLibraryIdentifier,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
);
|
|
142
|
-
return Boolean(result.modifiedCount);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const findUpdatedAppleWalletPasses = async (
|
|
146
|
-
passTypeIdentifier,
|
|
147
|
-
deviceLibraryIdentifier,
|
|
148
|
-
passesUpdatedSince,
|
|
149
|
-
): Promise<File[]> => {
|
|
150
|
-
const selector = {
|
|
151
|
-
$or: [
|
|
152
|
-
{
|
|
153
|
-
updated: passesUpdatedSince ? { $gt: passesUpdatedSince } : { $exists: true },
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
created: passesUpdatedSince ? { $gt: passesUpdatedSince } : { $exists: true },
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
'meta.registrations.deviceLibraryIdentifier': deviceLibraryIdentifier,
|
|
160
|
-
'meta.passTypeIdentifier': passTypeIdentifier,
|
|
161
|
-
path: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
162
|
-
};
|
|
163
|
-
const updatedPasses = await MediaObjects.find(selector).toArray();
|
|
164
|
-
return updatedPasses;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const invalidateAppleWalletPasses = async (unchainedAPI: UnchainedCore) => {
|
|
168
|
-
const allPasses = await MediaObjects.find({
|
|
169
|
-
path: APPLE_WALLET_PASSES_FILE_DIRECTORY,
|
|
170
|
-
}).toArray();
|
|
171
|
-
const allTokens = await TokenSurrogates.find({}).toArray();
|
|
172
|
-
|
|
173
|
-
for (const pass of allPasses) {
|
|
174
|
-
// Check if binary is already invalidated, if so, skip
|
|
175
|
-
const rawData = pass.meta?.rawData as TokenSurrogate;
|
|
176
|
-
if (rawData.invalidatedDate) continue;
|
|
177
|
-
|
|
178
|
-
const redeemedToken = allTokens.find((t) => t._id === rawData._id && t.invalidatedDate);
|
|
179
|
-
|
|
180
|
-
// Find invalidated token for the binary, if found, invalidate binary
|
|
181
|
-
if (redeemedToken) {
|
|
182
|
-
logger.info('Ticket redeemed, void pass', rawData);
|
|
183
|
-
await upsertAppleWalletPass(redeemedToken, unchainedAPI);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const buildMagicKey = async (orderId: string) => {
|
|
189
|
-
const msgUint8 = new TextEncoder().encode([orderId, process.env.UNCHAINED_SECRET].join(''));
|
|
190
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
|
191
|
-
// Convert ArrayBuffer to hex string without using Buffer
|
|
192
|
-
return Array.from(new Uint8Array(hashBuffer))
|
|
193
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
194
|
-
.join('');
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const cancelTicket = async (tokenId: string) => {
|
|
198
|
-
return TokenSurrogates.findOneAndUpdate(
|
|
199
|
-
{ _id: tokenId },
|
|
200
|
-
{ $set: { 'meta.cancelled': true } },
|
|
201
|
-
{
|
|
202
|
-
returnDocument: 'after',
|
|
203
|
-
},
|
|
204
|
-
);
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const isTicketCancelled = (token: TokenSurrogate) => {
|
|
208
|
-
return Boolean(token.meta?.cancelled);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const getTicketsCreated = async (
|
|
212
|
-
{
|
|
213
|
-
productId,
|
|
214
|
-
}: {
|
|
215
|
-
productId: string;
|
|
216
|
-
},
|
|
217
|
-
{ skipCancelled }: { skipCancelled?: boolean } = {},
|
|
218
|
-
): Promise<number> => {
|
|
219
|
-
const selector: any = {
|
|
220
|
-
productId,
|
|
221
|
-
};
|
|
222
|
-
if (skipCancelled) {
|
|
223
|
-
selector['meta.cancelled'] = null;
|
|
224
|
-
}
|
|
225
|
-
return TokenSurrogates.countDocuments(selector);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
upsertAppleWalletPass,
|
|
230
|
-
findAppleWalletPass,
|
|
231
|
-
registerDeviceForAppleWalletPass,
|
|
232
|
-
unregisterDeviceForAppleWalletPass,
|
|
233
|
-
findUpdatedAppleWalletPasses,
|
|
234
|
-
invalidateAppleWalletPasses,
|
|
235
|
-
upsertGoogleWalletPass,
|
|
236
|
-
buildMagicKey,
|
|
237
|
-
cancelTicket,
|
|
238
|
-
isTicketCancelled,
|
|
239
|
-
getTicketsCreated,
|
|
240
|
-
};
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
export interface TicketingModule {
|
|
244
|
-
passes: Awaited<ReturnType<typeof configurePasses>>;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export default {
|
|
248
|
-
passes: {
|
|
249
|
-
configure: configurePasses,
|
|
250
|
-
},
|
|
251
|
-
};
|