Added first version of serverless-based implementation example

This commit is contained in:
Alexander Cerutti
2021-10-31 19:37:29 +01:00
parent 32205871af
commit 8552f217ee
16 changed files with 22177 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { PKPass } from "../../../../lib";
import { finish400WithoutModelName, createPassGenerator } from "../shared";
/**
* Lambda for barcodes example
*/
export async function barcodes(event: ALBEvent) {
finish400WithoutModelName(event);
const { modelName, alt, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator(modelName, passOptions);
const pass = (await passGenerator.next()).value as PKPass;
if (alt === "true") {
// After this, pass.props["barcodes"] will have support for all the formats
pass.setBarcodes("Thank you for using this package <3");
console.log(
"Barcodes support is autocompleted:",
pass.props["barcodes"],
);
} else {
// After this, pass.props["barcodes"] will have support for just two of three
// of the passed format (the valid ones);
pass.setBarcodes(
{
message: "Thank you for using this package <3",
format: "PKBarcodeFormatCode128",
},
{
message: "Thank you for using this package <3",
format: "PKBarcodeFormatPDF417",
},
);
}
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,31 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { Context } from "vm";
import { PKPass } from "passkit-generator";
import { finish400WithoutModelName, createPassGenerator } from "../shared";
/**
* Lambda for expirationDate example
*/
export async function expirationDate(event: ALBEvent, context: Context) {
finish400WithoutModelName(event);
const { modelName, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator(modelName, passOptions);
const pass = (await passGenerator.next()).value as PKPass;
// 2 minutes later...
const d = new Date();
d.setMinutes(d.getMinutes() + 2);
// setting the expiration
(pass as PKPass).setExpirationDate(d);
console.log(
"EXPIRATION DATE EXPECTED:",
(pass as PKPass).props["expirationDate"],
);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,160 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { PKPass } from "passkit-generator";
import { createPassGenerator } from "../shared";
/**
* Lambda for fields example
*/
export async function fields(event: ALBEvent) {
const { modelName, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator("exampleBooking", passOptions);
const pass = (await passGenerator.next()).value as PKPass;
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push(
{
key: "header1",
label: "Data",
value: "25 mag",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "header2",
label: "Volo",
value: "EZY997",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.primaryFields.push(
{
key: "IATA-source",
value: "NAP",
label: "Napoli",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "IATA-destination",
value: "VCE",
label: "Venezia Marco Polo",
textAlignment: "PKTextAlignmentRight",
},
);
pass.secondaryFields.push(
{
key: "secondary1",
label: "Imbarco chiuso",
value: "18:40",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "sec2",
label: "Partenze",
value: "19:10",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "sec3",
label: "SB",
value: "Sì",
textAlignment: "PKTextAlignmentCenter",
},
{
key: "sec4",
label: "Imbarco",
value: "Anteriore",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.auxiliaryFields.push(
{
key: "aux1",
label: "Passeggero",
value: "MR. WHO KNOWS",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "aux2",
label: "Posto",
value: "1A*",
textAlignment: "PKTextAlignmentCenter",
},
);
pass.backFields.push(
{
key: "document number",
label: "Numero documento:",
value: "- -",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "You're checked in, what next",
label: "Hai effettuato il check-in, Quali sono le prospettive",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Check In",
label: "1. check-in✓",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "checkIn",
label: "",
value: "Le uscite d'imbarco chiudono 30 minuti prima della partenza, quindi sii puntuale. In questo aeroporto puoi utilizzare la corsia Fast Track ai varchi di sicurezza.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "2. Bags",
label: "2. Bagaglio",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Require special assistance",
label: "Assistenza speciale",
value: "Se hai richiesto assistenza speciale, presentati a un membro del personale nell'area di Consegna bagagli almeno 90 minuti prima del volo.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "3. Departures",
label: "3. Partenze",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "photoId",
label: "Un documento didentità corredato di fotografia",
value: "è obbligatorio su TUTTI i voli. Per un viaggio internazionale è necessario un passaporto valido o, dove consentita, una carta didentità.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "yourSeat",
label: "Il tuo posto:",
value: "verifica il tuo numero di posto nella parte superiore. Durante limbarco utilizza le scale anteriori e posteriori: per le file 1-10 imbarcati dalla parte anteriore; per le file 11-31 imbarcati dalla parte posteriore. Colloca le borse di dimensioni ridotte sotto il sedile davanti a te.",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Pack safely",
label: "Bagaglio sicuro",
value: "Fai clic http://easyjet.com/it/articoli-pericolosi per maggiori informazioni sulle merci pericolose oppure visita il sito CAA http://www.caa.co.uk/default.aspx?catid=2200",
textAlignment: "PKTextAlignmentLeft",
},
{
key: "Thank you for travelling easyJet",
label: "Grazie per aver viaggiato con easyJet",
value: "",
textAlignment: "PKTextAlignmentLeft",
},
);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,6 @@
export * from "./barcodes";
export * from "./expirationDate";
export * from "./fields";
export * from "./localize";
export * from "./pkpasses";
export * from "./scratch";

View File

@@ -0,0 +1,45 @@
import { finish400WithoutModelName, createPassGenerator } from "../shared";
import type { ALBEvent, ALBResult } from "aws-lambda";
import type { PKPass } from "passkit-generator";
import { localizationSymbol } from "passkit-generator/lib/PKPass";
/**
* Lambda for localize example
*/
export async function localize(event: ALBEvent) {
finish400WithoutModelName(event);
const { modelName, ...passOptions } = event.queryStringParameters;
const passGenerator = createPassGenerator(modelName, passOptions);
const pass = (await passGenerator.next()).value as PKPass;
/**
* Italian and German already has an .lproj which gets included
* but it doesn't have translations
*/
pass.localize("it", {
EVENT: "Evento",
LOCATION: "Dove",
});
pass.localize("de", {
EVENT: "Ereignis",
LOCATION: "Ort",
});
// ...while Norwegian doesn't, so it gets created
pass.localize("nn", {
EVENT: "Begivenhet",
LOCATION: "plassering",
});
console.log(
"Added languages",
Object.keys(pass[localizationSymbol]).join(", "),
);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1,115 @@
import { ALBEvent } from "aws-lambda";
import { PKPass } from "passkit-generator";
import {
getCertificates,
getSpecificFileInModel,
getS3Instance,
getRandomColorPart,
finish400WithoutModelName,
} from "../shared";
import config from "../../config.json";
/**
* Lambda for PkPasses example
*/
export async function pkpasses(event: ALBEvent) {
finish400WithoutModelName(event);
const [certificates, iconFromModel, s3] = await Promise.all([
getCertificates(),
getSpecificFileInModel(
"icon.png",
event.queryStringParameters.modelName,
),
getS3Instance(),
]);
function createPass() {
const pass = new PKPass({}, certificates, {
description: "Example Apple Wallet Pass",
passTypeIdentifier: "pass.com.passkitgenerator",
serialNumber: "nmyuxofgna",
organizationName: `Test Organization ${Math.random()}`,
teamIdentifier: "F53WB8AE67",
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
});
pass.type = "boardingPass";
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push(
{
key: "header-field-test-1",
value: "Unknown",
},
{
key: "header-field-test-2",
value: "unknown",
},
);
pass.primaryFields.push(
{
key: "primaryField-1",
value: "NAP",
},
{
key: "primaryField-2",
value: "VCE",
},
);
/**
* Required by Apple. If one is not available, a
* pass might be openable on a Mac but not on a
* specific iPhone model
*/
pass.addBuffer("icon.png", iconFromModel);
pass.addBuffer("icon@2x.png", iconFromModel);
pass.addBuffer("icon@3x.png", iconFromModel);
return pass;
}
const passes = await Promise.all([
Promise.resolve(createPass()),
Promise.resolve(createPass()),
Promise.resolve(createPass()),
Promise.resolve(createPass()),
]);
const pkpasses = PKPass.pack(...passes);
const buffer = pkpasses.getAsBuffer();
const passName = `GeneratedPass-${Math.random()}.pkpass`;
const { Location } = await s3
.upload({
Bucket: config.PASSES_S3_TEMP_BUCKET,
Key: passName,
ContentType: pkpasses.mimeType,
/** Marking it as expiring in 5 minutes, because passes should not be stored */
Expires: new Date(Date.now() + 5 * 60 * 1000),
Body: buffer,
})
.promise();
/**
* Please note that redirection to `Location` does not work
* if you open this code in another device if this is run
* offline. This because `Location` is on localhost. Didn't
* find yet a way to solve this.
*/
return {
statusCode: 302,
headers: {
"Content-Type": "application/vnd.apple.pkpass",
Location: Location,
},
};
}

View File

@@ -0,0 +1,71 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import { PKPass } from "passkit-generator";
import {
createPassGenerator,
getRandomColorPart,
getSpecificFileInModel,
} from "../shared";
/**
* Lambda for scratch example
*/
export async function scratch(event: ALBEvent) {
const passGenerator = createPassGenerator(undefined, {
description: "Example Apple Wallet Pass",
passTypeIdentifier: "pass.com.passkitgenerator",
serialNumber: "nmyuxofgna",
organizationName: `Test Organization ${Math.random()}`,
teamIdentifier: "F53WB8AE67",
foregroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
labelColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
backgroundColor: `rgb(${getRandomColorPart()}, ${getRandomColorPart()}, ${getRandomColorPart()})`,
});
const [{ value }, iconFromModel] = await Promise.all([
passGenerator.next(),
getSpecificFileInModel(
"icon.png",
event.queryStringParameters.modelName,
),
]);
const pass = value as PKPass;
pass.type = "boardingPass";
pass.transitType = "PKTransitTypeAir";
pass.headerFields.push(
{
key: "header-field-test-1",
value: "Unknown",
},
{
key: "header-field-test-2",
value: "unknown",
},
);
pass.primaryFields.push(
{
key: "primaryField-1",
value: "NAP",
},
{
key: "primaryField-2",
value: "VCE",
},
);
/**
* Required by Apple. If one is not available, a
* pass might be openable on a Mac but not on a
* specific iPhone model
*/
pass.addBuffer("icon.png", iconFromModel);
pass.addBuffer("icon@2x.png", iconFromModel);
pass.addBuffer("icon@3x.png", iconFromModel);
return (await passGenerator.next(pass as PKPass)).value as ALBResult;
}

View File

@@ -0,0 +1 @@
export * from "./functions";

View File

@@ -0,0 +1,176 @@
import { ALBEvent, ALBResult } from "aws-lambda";
import AWS from "aws-sdk";
import { promises as fs } from "fs";
import path from "path";
import config from "../config.json";
import { PKPass } from "passkit-generator";
const S3: { instance: AWS.S3 } = { instance: undefined };
export function finish400WithoutModelName(event: ALBEvent) {
if (event.queryStringParameters?.modelName) {
return;
}
return {
statusCode: 400,
body: JSON.stringify({
message: "modelName is missing in query params",
}),
};
}
export function getRandomColorPart() {
return Math.floor(Math.random() * 255);
}
export async function getModel(
modelName: string,
): Promise<string | { [key: string]: Buffer }> {
if (process.env.IS_OFFLINE === "true") {
console.log("model offline retrieving");
return path.resolve(__dirname, `../../models/${modelName}`);
}
const s3 = await getS3Instance();
const result = await s3
.getObject({ Bucket: config.MODELS_S3_BUCKET, Key: modelName })
.promise();
return {}; // @TODO, like when it is run on s3
}
export async function getCertificates(): Promise<{
signerCert: string | Buffer;
signerKey: string | Buffer;
wwdr: string | Buffer;
signerKeyPassphrase?: string;
}> {
let signerCert: string;
let signerKey: string;
let wwdr: string;
let signerKeyPassphrase: string;
if (process.env.IS_OFFLINE) {
console.log("Fetching Certificates locally");
[signerCert, signerKey, wwdr, signerKeyPassphrase] = await Promise.all([
fs.readFile(
path.resolve(__dirname, "../../../certificates/signerCert.pem"),
"utf-8",
),
fs.readFile(
path.resolve(__dirname, "../../../certificates/signerKey.pem"),
"utf-8",
),
fs.readFile(
path.resolve(__dirname, "../../../certificates/WWDR.pem"),
"utf-8",
),
Promise.resolve(config.SIGNER_KEY_PASSPHRASE),
]);
} else {
// @TODO
}
return {
signerCert,
signerKey,
wwdr,
signerKeyPassphrase,
};
}
export async function getS3Instance() {
if (S3.instance) {
return S3.instance;
}
const instance = new AWS.S3({
s3ForcePathStyle: true,
accessKeyId: process.env.IS_OFFLINE ? "S3RVER" : config.ACCESS_KEY_ID, // This specific key is required when working offline
secretAccessKey: config.SECRET_ACCESS_KEY,
endpoint: new AWS.Endpoint("http://localhost:4569"),
});
S3.instance = instance;
try {
/** Trying to create a new bucket. If it fails, it already exists (at least in theory) */
await instance
.createBucket({ Bucket: config.PASSES_S3_TEMP_BUCKET })
.promise();
} catch (err) {}
return instance;
}
export async function getSpecificFileInModel(
fileName: string,
modelName: string,
) {
const model = await getModel(modelName);
if (typeof model === "string") {
return fs.readFile(path.resolve(`${model}.pass`, fileName));
}
return model[fileName];
}
export async function* createPassGenerator(
modelName?: string,
passOptions?: Object,
): AsyncGenerator<PKPass, ALBResult, PKPass> {
const [template, certificates, s3] = await Promise.all([
modelName ? getModel(modelName) : Promise.resolve({}),
getCertificates(),
getS3Instance(),
]);
let pass: PKPass;
if (template instanceof Object) {
pass = new PKPass(template, certificates, passOptions);
} else if (typeof template === "string") {
pass = await PKPass.from(
{
model: template,
certificates,
},
passOptions,
);
}
pass = yield pass;
const buffer = pass.getAsBuffer();
const passName = `GeneratedPass-${Math.random()}.pkpass`;
const { Location } = await s3
.upload({
Bucket: config.PASSES_S3_TEMP_BUCKET,
Key: passName,
ContentType: pass.mimeType,
/** Marking it as expiring in 5 minutes, because passes should not be stored */
Expires: new Date(Date.now() + 5 * 60 * 1000),
Body: buffer,
})
.promise();
/**
* Please note that redirection to `Location` does not work
* if you open this code in another device if this is run
* offline. This because `Location` is on localhost. Didn't
* find yet a way to solve this.
*/
return {
statusCode: 302,
headers: {
"Content-Type": "application/vnd.apple.pkpass",
Location: Location,
},
};
}