@unchainedshop/ticketing 4.6.1 → 4.6.2
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/lib/routes.d.ts +16 -0
- package/lib/routes.js +288 -0
- package/package.json +1 -1
package/lib/routes.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context } from '@unchainedshop/api';
|
|
2
|
+
import type { TicketingAPI } from './index.ts';
|
|
3
|
+
export declare function printTicketsHandler(request: Request, context: Context): Promise<Response>;
|
|
4
|
+
export declare function googleWalletHandler(request: Request, context: TicketingAPI & {
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
}): Promise<Response>;
|
|
7
|
+
export declare function appleWalletHandler(request: Request, context: TicketingAPI & {
|
|
8
|
+
params: Record<string, string>;
|
|
9
|
+
}): Promise<Response>;
|
|
10
|
+
export declare const ticketingRoutes: ({
|
|
11
|
+
path: string;
|
|
12
|
+
handler: typeof printTicketsHandler;
|
|
13
|
+
} | {
|
|
14
|
+
path: string;
|
|
15
|
+
handler: typeof googleWalletHandler;
|
|
16
|
+
})[];
|
package/lib/routes.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { checkAction } from '@unchainedshop/api/lib/acl.js';
|
|
3
|
+
import { actions } from '@unchainedshop/api/lib/roles/index.js';
|
|
4
|
+
import { RendererTypes, getRenderer } from "./template-registry.js";
|
|
5
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
6
|
+
import { getFileAdapter } from '@unchainedshop/core';
|
|
7
|
+
const logger = createLogger('unchained:ticketing');
|
|
8
|
+
const { APPLE_WALLET_WEBSERVICE_PATH = '/rest/apple-wallet', GOOGLE_WALLET_WEBSERVICE_PATH = '/rest/google-wallet', UNCHAINED_PDF_PRINT_HANDLER_PATH = '/rest/print_tickets', } = process.env;
|
|
9
|
+
const isAuthenticationTokenCorrect = (authHeader, authenticationToken) => {
|
|
10
|
+
const expectedAuthorizationValue = `ApplePass ${authenticationToken}`;
|
|
11
|
+
return authHeader === expectedAuthorizationValue;
|
|
12
|
+
};
|
|
13
|
+
export async function printTicketsHandler(request, context) {
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
const variant = url.searchParams.get('variant');
|
|
16
|
+
const orderId = url.searchParams.get('orderId');
|
|
17
|
+
const otp = url.searchParams.get('otp');
|
|
18
|
+
try {
|
|
19
|
+
if (!orderId || !otp) {
|
|
20
|
+
throw new Error('Missing required query parameters: orderId and otp');
|
|
21
|
+
}
|
|
22
|
+
await checkAction(context, actions.viewOrder, [undefined, { orderId, otp }]);
|
|
23
|
+
const render = getRenderer(RendererTypes.ORDER_PDF);
|
|
24
|
+
const pdfStream = await render({ orderId, variant: variant }, context);
|
|
25
|
+
const webStream = Readable.toWeb(pdfStream);
|
|
26
|
+
return new Response(webStream, {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/pdf',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger.error(error);
|
|
35
|
+
return new Response(null, { status: 403 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function googleWalletHandler(request, context) {
|
|
39
|
+
const { modules } = context;
|
|
40
|
+
const { tokenId } = context.params;
|
|
41
|
+
try {
|
|
42
|
+
if (!tokenId) {
|
|
43
|
+
return new Response(JSON.stringify({ error: 'Token ID required' }), {
|
|
44
|
+
status: 404,
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const token = await modules.warehousing.findToken({ tokenId });
|
|
49
|
+
if (!token) {
|
|
50
|
+
return new Response(JSON.stringify({ error: 'Token not found' }), {
|
|
51
|
+
status: 404,
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const url = new URL(request.url);
|
|
56
|
+
const hash = url.searchParams.get('hash');
|
|
57
|
+
const correctHash = await modules.warehousing.buildAccessKeyForToken(tokenId);
|
|
58
|
+
if (!hash || hash !== correctHash) {
|
|
59
|
+
return new Response(JSON.stringify({ error: 'Token hash invalid for current owner' }), {
|
|
60
|
+
status: 403,
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const passLink = await modules.passes.upsertGoogleWalletPass(token, context);
|
|
65
|
+
return new Response(JSON.stringify({ passLink }), {
|
|
66
|
+
status: 200,
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
logger.error(e);
|
|
72
|
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
73
|
+
status: 500,
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function appleWalletHandler(request, context) {
|
|
79
|
+
const { modules } = context;
|
|
80
|
+
const url = new URL(request.url);
|
|
81
|
+
const pathname = url.pathname;
|
|
82
|
+
logger.info(`${pathname} (${JSON.stringify(Object.fromEntries(url.searchParams))})`);
|
|
83
|
+
if (pathname.includes('/download/')) {
|
|
84
|
+
try {
|
|
85
|
+
const pathParts = pathname.split('/');
|
|
86
|
+
const passFileName = pathParts[pathParts.length - 1];
|
|
87
|
+
const [tokenId] = passFileName.split('.pkpass');
|
|
88
|
+
if (!tokenId) {
|
|
89
|
+
return new Response(null, { status: 404 });
|
|
90
|
+
}
|
|
91
|
+
const token = await modules.warehousing.findToken({ tokenId });
|
|
92
|
+
if (!token) {
|
|
93
|
+
return new Response(JSON.stringify({ error: 'Token not found' }), {
|
|
94
|
+
status: 404,
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const hash = url.searchParams.get('hash');
|
|
99
|
+
const correctHash = await modules.warehousing.buildAccessKeyForToken(tokenId);
|
|
100
|
+
if (!hash || hash !== correctHash) {
|
|
101
|
+
return new Response(JSON.stringify({ error: 'Token hash invalid for current owner' }), {
|
|
102
|
+
status: 403,
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const passFile = await modules.passes.upsertAppleWalletPass(token, context);
|
|
107
|
+
const fileUploadAdapter = getFileAdapter();
|
|
108
|
+
const signedUrl = await fileUploadAdapter.createDownloadURL(passFile);
|
|
109
|
+
const downloadUrl = signedUrl && (await modules.files.normalizeUrl(signedUrl, {}));
|
|
110
|
+
if (!downloadUrl) {
|
|
111
|
+
return new Response(JSON.stringify({
|
|
112
|
+
success: false,
|
|
113
|
+
message: 'Could not create download URL',
|
|
114
|
+
name: 'URL_SIGNING_FAILED',
|
|
115
|
+
}), {
|
|
116
|
+
status: 500,
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const response = await fetch(downloadUrl);
|
|
121
|
+
const data = await response.arrayBuffer();
|
|
122
|
+
return new Response(data, {
|
|
123
|
+
status: 200,
|
|
124
|
+
headers: {
|
|
125
|
+
'Content-Type': 'application/vnd.apple.pkpass',
|
|
126
|
+
'Content-Disposition': `attachment; filename=${tokenId}.pkpass`,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
logger.error(e);
|
|
132
|
+
return new Response(null, { status: 500 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const pathParts = pathname.split('/').filter(Boolean);
|
|
136
|
+
const [, , endpoint, ...pathComponents] = pathParts;
|
|
137
|
+
if (endpoint === 'devices') {
|
|
138
|
+
if (request.method === 'POST') {
|
|
139
|
+
const [deviceLibraryIdentifier, , passTypeIdentifier, serialNumber] = pathComponents;
|
|
140
|
+
try {
|
|
141
|
+
const body = await request.json();
|
|
142
|
+
const { pushToken } = body;
|
|
143
|
+
const pass = await modules.passes.findAppleWalletPass(passTypeIdentifier, serialNumber);
|
|
144
|
+
if (!pass) {
|
|
145
|
+
return new Response(null, { status: 404 });
|
|
146
|
+
}
|
|
147
|
+
const authToken = pass.meta?.rawData?._id || pass.meta?.rawData?.tokenId;
|
|
148
|
+
if (!isAuthenticationTokenCorrect(request.headers.get('authorization'), authToken)) {
|
|
149
|
+
return new Response(null, { status: 401 });
|
|
150
|
+
}
|
|
151
|
+
const newRegistration = await modules.passes.registerDeviceForAppleWalletPass(passTypeIdentifier, serialNumber, {
|
|
152
|
+
deviceLibraryIdentifier,
|
|
153
|
+
pushToken,
|
|
154
|
+
});
|
|
155
|
+
return new Response(null, { status: newRegistration ? 201 : 200 });
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
logger.error(e);
|
|
159
|
+
return new Response(null, { status: 500 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (request.method === 'GET') {
|
|
163
|
+
const [deviceLibraryIdentifier, , passTypeIdentifier] = pathComponents;
|
|
164
|
+
const passesUpdatedSinceParam = url.searchParams.get('passesUpdatedSince');
|
|
165
|
+
const passesUpdatedSince = passesUpdatedSinceParam ? new Date(passesUpdatedSinceParam) : undefined;
|
|
166
|
+
try {
|
|
167
|
+
const passes = await modules.passes.findUpdatedAppleWalletPasses(passTypeIdentifier, deviceLibraryIdentifier, passesUpdatedSince);
|
|
168
|
+
const serialNumbers = passes.map((t) => t.meta?.serialNumber).filter(Boolean);
|
|
169
|
+
if (serialNumbers?.length) {
|
|
170
|
+
return new Response(JSON.stringify({
|
|
171
|
+
serialNumbers,
|
|
172
|
+
lastUpdated: new Date(),
|
|
173
|
+
}), {
|
|
174
|
+
status: 200,
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return new Response(null, { status: 204 });
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
logger.error(e);
|
|
182
|
+
return new Response(null, { status: 500 });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (request.method === 'DELETE') {
|
|
186
|
+
const [deviceLibraryIdentifier, , passTypeIdentifier, serialNumber] = pathComponents;
|
|
187
|
+
try {
|
|
188
|
+
const pass = await modules.passes.findAppleWalletPass(passTypeIdentifier, serialNumber);
|
|
189
|
+
if (!pass) {
|
|
190
|
+
return new Response(null, { status: 404 });
|
|
191
|
+
}
|
|
192
|
+
const authToken = pass.meta?.rawData?._id || pass.meta?.rawData?.tokenId;
|
|
193
|
+
if (!isAuthenticationTokenCorrect(request.headers.get('authorization'), authToken)) {
|
|
194
|
+
return new Response(null, { status: 401 });
|
|
195
|
+
}
|
|
196
|
+
await modules.passes.unregisterDeviceForAppleWalletPass(passTypeIdentifier, serialNumber, deviceLibraryIdentifier);
|
|
197
|
+
return new Response(null, { status: 200 });
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
logger.error(e);
|
|
201
|
+
return new Response(null, { status: 500 });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (endpoint === 'log') {
|
|
206
|
+
try {
|
|
207
|
+
const body = await request.json();
|
|
208
|
+
const { logs } = body;
|
|
209
|
+
logs?.forEach((log) => {
|
|
210
|
+
if (typeof log === 'string') {
|
|
211
|
+
logger.info(log);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return new Response(null, { status: 200 });
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
logger.error(e);
|
|
218
|
+
return new Response(null, { status: 500 });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (endpoint === 'passes') {
|
|
222
|
+
if (request.method === 'GET') {
|
|
223
|
+
const [passTypeIdentifier, serialNumber] = pathComponents;
|
|
224
|
+
try {
|
|
225
|
+
const pass = await modules.passes.findAppleWalletPass(passTypeIdentifier, serialNumber);
|
|
226
|
+
if (!pass) {
|
|
227
|
+
return new Response(null, { status: 404 });
|
|
228
|
+
}
|
|
229
|
+
const authToken = pass.meta?.rawData?._id || pass.meta?.rawData?.tokenId;
|
|
230
|
+
if (!isAuthenticationTokenCorrect(request.headers.get('authorization'), authToken)) {
|
|
231
|
+
return new Response(null, { status: 401 });
|
|
232
|
+
}
|
|
233
|
+
const { updated, created } = pass;
|
|
234
|
+
const lastModifiedDate = new Date(updated || created);
|
|
235
|
+
lastModifiedDate.setMilliseconds(0);
|
|
236
|
+
const ifModifiedSinceHeader = request.headers.get('if-modified-since');
|
|
237
|
+
if (ifModifiedSinceHeader) {
|
|
238
|
+
const ifModifiedSinceDate = new Date(ifModifiedSinceHeader);
|
|
239
|
+
ifModifiedSinceDate.setMilliseconds(0);
|
|
240
|
+
if (ifModifiedSinceDate.getTime() >= lastModifiedDate.getTime()) {
|
|
241
|
+
return new Response(null, { status: 304 });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const fileUploadAdapter = getFileAdapter();
|
|
245
|
+
const signedUrl = await fileUploadAdapter.createDownloadURL(pass);
|
|
246
|
+
const downloadUrl = signedUrl && (await modules.files.normalizeUrl(signedUrl, {}));
|
|
247
|
+
if (!downloadUrl) {
|
|
248
|
+
return new Response(JSON.stringify({
|
|
249
|
+
success: false,
|
|
250
|
+
message: 'Could not create download URL',
|
|
251
|
+
name: 'URL_SIGNING_FAILED',
|
|
252
|
+
}), {
|
|
253
|
+
status: 500,
|
|
254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const result = await fetch(downloadUrl);
|
|
258
|
+
const data = await result.arrayBuffer();
|
|
259
|
+
return new Response(data, {
|
|
260
|
+
status: 200,
|
|
261
|
+
headers: {
|
|
262
|
+
'Content-Type': 'application/vnd.apple.pkpass',
|
|
263
|
+
'Last-Modified': lastModifiedDate.toUTCString(),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
logger.error(e);
|
|
269
|
+
return new Response(null, { status: 500 });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return new Response(null, { status: 404 });
|
|
274
|
+
}
|
|
275
|
+
export const ticketingRoutes = [
|
|
276
|
+
{
|
|
277
|
+
path: UNCHAINED_PDF_PRINT_HANDLER_PATH,
|
|
278
|
+
handler: printTicketsHandler,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
path: `${GOOGLE_WALLET_WEBSERVICE_PATH}/download/:tokenId`,
|
|
282
|
+
handler: googleWalletHandler,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
path: `${APPLE_WALLET_WEBSERVICE_PATH}/*`,
|
|
286
|
+
handler: appleWalletHandler,
|
|
287
|
+
},
|
|
288
|
+
];
|
package/package.json
CHANGED