mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 14:25:17 +00:00
5
examples/firebase/.firebaserc
Normal file
5
examples/firebase/.firebaserc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "<put here your project name>"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
examples/firebase/.gitignore
vendored
Normal file
63
examples/firebase/.gitignore
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
firebase-debug.log*
|
||||||
|
firebase-debug.*.log*
|
||||||
|
|
||||||
|
# Firebase cache
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# Firebase config
|
||||||
|
|
||||||
|
# Uncomment this if you'd like others to create their own Firebase project.
|
||||||
|
# For a team working on the same Firebase project(s), it is recommended to leave
|
||||||
|
# it commented so all members can deploy to the same project(s) in .firebaserc.
|
||||||
|
# .firebaserc
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
135
examples/firebase/README.md
Normal file
135
examples/firebase/README.md
Normal file
File diff suppressed because one or more lines are too long
22
examples/firebase/firebase.json
Normal file
22
examples/firebase/firebase.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"source": "functions",
|
||||||
|
"codebase": "default",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git",
|
||||||
|
"firebase-debug.log",
|
||||||
|
"firebase-debug.*.log"
|
||||||
|
],
|
||||||
|
"predeploy": [
|
||||||
|
"cp -r ../models functions/models",
|
||||||
|
"npm --prefix \"$RESOURCE_DIR\" run build"
|
||||||
|
],
|
||||||
|
"postdeploy": ["rm -rf functions/models"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storage": {
|
||||||
|
"rules": "storage.rules"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
examples/firebase/functions/.env
Normal file
25
examples/firebase/functions/.env
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Please note that these could be considered like
|
||||||
|
# secrets, so using an .env file might not be the
|
||||||
|
# best solution.
|
||||||
|
#
|
||||||
|
# It is fine for the purpose of demostrating how
|
||||||
|
# a firebase cloud functions is created and
|
||||||
|
# deployed when using passkit-generator
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# For production you might want to check how to
|
||||||
|
# deploy secrets.
|
||||||
|
#
|
||||||
|
# Check: https://firebase.google.com/docs/functions/config-env?gen=2nd#secret-manager
|
||||||
|
|
||||||
|
#TODO change this with the content of your WWDR certificate
|
||||||
|
WWDR=""
|
||||||
|
|
||||||
|
#TODO add inside quotes the content of your signer certificate
|
||||||
|
SIGNER_CERT=""
|
||||||
|
|
||||||
|
#TODO add inside quotes the content of your signer key certificate
|
||||||
|
SIGNER_KEY=""
|
||||||
|
|
||||||
|
#TODO change this with the passphrase of your SIGNER_KEY
|
||||||
|
SIGNER_KEY_PASSPHRASE=123456
|
||||||
9
examples/firebase/functions/.gitignore
vendored
Normal file
9
examples/firebase/functions/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Compiled JavaScript files
|
||||||
|
lib/**/*.js
|
||||||
|
lib/**/*.js.map
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Node.js dependency directory
|
||||||
|
node_modules/
|
||||||
14269
examples/firebase/functions/package-lock.json
generated
Normal file
14269
examples/firebase/functions/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
examples/firebase/functions/package.json
Normal file
34
examples/firebase/functions/package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "functions",
|
||||||
|
"description": "Cloud Functions for Firebase",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "npm run build && npx firebase emulators:start --only functions",
|
||||||
|
"shell": "npm run build && npx firebase functions:shell",
|
||||||
|
"deploy": "npx firebase deploy --only functions",
|
||||||
|
"logs": "npx firebase functions:log",
|
||||||
|
"service:link-pg": "cd ../../.. && npm run build && npm link",
|
||||||
|
"predev:install": "npm run clear:deps",
|
||||||
|
"dev:install": "npm run service:link-pg && npm link passkit-generator",
|
||||||
|
"clear:deps": "rm -rf node_modules",
|
||||||
|
"build": "rm -rf lib && npx tsc"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "16"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.2.6",
|
||||||
|
"firebase-admin": "^11.10.1",
|
||||||
|
"firebase-functions": "^4.4.1",
|
||||||
|
"tslib": "^2.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"passkit-generator": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"firebase-functions-test": "^0.2.3",
|
||||||
|
"firebase-tools": "^12.4.6"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
320
examples/firebase/functions/src/index.ts
Normal file
320
examples/firebase/functions/src/index.ts
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import * as functions from "firebase-functions";
|
||||||
|
import { initializeApp } from "firebase-admin/app";
|
||||||
|
import { getStorage } from "firebase-admin/storage";
|
||||||
|
import passkit from "passkit-generator";
|
||||||
|
import type { Barcode, TransitType } from "passkit-generator";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
// Please note this is experimental in NodeJS as
|
||||||
|
// it is marked as Stage 3 in TC39
|
||||||
|
// Should probably not be used in production
|
||||||
|
import startData from "./startData.json" assert { "type": "json" };
|
||||||
|
|
||||||
|
const PKPass = passkit.PKPass;
|
||||||
|
|
||||||
|
interface RequestWithBody extends functions.Request {
|
||||||
|
body: {
|
||||||
|
passModel: string;
|
||||||
|
serialNumber: string;
|
||||||
|
logoText: string;
|
||||||
|
textColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
labelColor: string;
|
||||||
|
relevantDate?: string;
|
||||||
|
expiryDate?: string;
|
||||||
|
relevantLocationLat?: number;
|
||||||
|
relevantLocationLong?: number;
|
||||||
|
header?: { value: string; label: string }[];
|
||||||
|
primary?: { value: string; label: string }[];
|
||||||
|
secondary?: { value: string; label: string }[];
|
||||||
|
auxiliary?: { value: string; label: string }[];
|
||||||
|
codeAlt?: string;
|
||||||
|
qrText?: string;
|
||||||
|
transitType?: TransitType;
|
||||||
|
codeType?: Barcode["format"];
|
||||||
|
thumbnailFile?: string;
|
||||||
|
logoFile?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declaring our .env contents
|
||||||
|
* @see https://firebase.google.com/docs/functions/config-env?gen=2nd#deploying_multiple_sets_of_environment_variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
WWDR: string;
|
||||||
|
SIGNER_CERT: string;
|
||||||
|
SIGNER_KEY: string;
|
||||||
|
SIGNER_KEY_PASSPHRASE: string;
|
||||||
|
|
||||||
|
// reserved, but we use it to discriminate emulator from deploy
|
||||||
|
FUNCTIONS_EMULATOR: "true" | "false" | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://firebase.google.com/docs/storage/admin/start#node.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
initializeApp({
|
||||||
|
storageBucket: startData.FIREBASE_BUCKET_ADDR,
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageRef = getStorage().bucket();
|
||||||
|
|
||||||
|
export const pass = functions.https.onRequest(
|
||||||
|
async (request: RequestWithBody, response) => {
|
||||||
|
let modelBasePath: string;
|
||||||
|
|
||||||
|
if (process.env.FUNCTIONS_EMULATOR === "true") {
|
||||||
|
modelBasePath = "../../models/";
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Models are cloned on deploy through
|
||||||
|
* the commands in `firebase.json` and
|
||||||
|
* are uploaded along with our program.
|
||||||
|
*
|
||||||
|
* When deployed, root folder is the `functions` folder
|
||||||
|
*/
|
||||||
|
modelBasePath = "./models/";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (request.headers["content-type"] !== "application/json") {
|
||||||
|
response.status(400);
|
||||||
|
response.send({
|
||||||
|
error: `Payload with content-type ${request.headers["content-type"]} is not supported. Use "application/json"`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.body.passModel) {
|
||||||
|
response.status(400);
|
||||||
|
response.send({
|
||||||
|
error: "Unspecified 'passModel' parameter: which model should be used?",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.body.passModel.endsWith(".pass")) {
|
||||||
|
request.body.passModel = request.body.passModel.replace(
|
||||||
|
".pass",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPass = await PKPass.from(
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get relevant pass model from model folder (see passkit-generator/examples/models/)
|
||||||
|
* Path seems to get read like the function is in "firebase/" folder and not in "firebase/functions/"
|
||||||
|
*/
|
||||||
|
model: `${modelBasePath}${request.body.passModel}.pass`,
|
||||||
|
certificates: {
|
||||||
|
// Assigning certificates from certs folder (you will need to provide these yourself)
|
||||||
|
wwdr: process.env.WWDR,
|
||||||
|
signerCert: process.env.SIGNER_CERT,
|
||||||
|
signerKey: process.env.SIGNER_KEY,
|
||||||
|
signerKeyPassphrase: process.env.SIGNER_KEY_PASSPHRASE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serialNumber: request.body.serialNumber,
|
||||||
|
description: "DESCRIPTION",
|
||||||
|
logoText: request.body.logoText,
|
||||||
|
foregroundColor: request.body.textColor,
|
||||||
|
backgroundColor: request.body.backgroundColor,
|
||||||
|
labelColor: request.body.labelColor,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newPass.type == "boardingPass") {
|
||||||
|
if (!request.body.transitType) {
|
||||||
|
response.status(400);
|
||||||
|
response.send({
|
||||||
|
error: "transitType is required",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPass.transitType = request.body.transitType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof request.body.relevantDate === "string") {
|
||||||
|
newPass.setRelevantDate(new Date(request.body.relevantDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof request.body.expiryDate === "string") {
|
||||||
|
newPass.setExpirationDate(new Date(request.body.expiryDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.body.relevantLocationLat &&
|
||||||
|
request.body.relevantLocationLong
|
||||||
|
) {
|
||||||
|
newPass.setLocations({
|
||||||
|
latitude: request.body.relevantLocationLat,
|
||||||
|
longitude: request.body.relevantLocationLong,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(request.body.header)) {
|
||||||
|
for (let i = 0; i < request.body.header.length; i++) {
|
||||||
|
const field = request.body.header[i];
|
||||||
|
|
||||||
|
if (!(field?.label && field.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPass.headerFields.push({
|
||||||
|
key: `header${i}`,
|
||||||
|
label: field.label,
|
||||||
|
value: field.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(request.body.primary)) {
|
||||||
|
for (let i = 0; i < request.body.primary.length; i++) {
|
||||||
|
const field = request.body.primary[i];
|
||||||
|
|
||||||
|
if (!(field?.label && field.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPass.primaryFields.push({
|
||||||
|
key: `primary${i}`,
|
||||||
|
label: field.label,
|
||||||
|
value:
|
||||||
|
newPass.type == "boardingPass"
|
||||||
|
? field.value.toUpperCase()
|
||||||
|
: field.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(request.body.secondary)) {
|
||||||
|
for (let i = 0; i < request.body.secondary.length; i++) {
|
||||||
|
const field = request.body.secondary[i];
|
||||||
|
|
||||||
|
if (!(field?.label && field.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isElementInLastTwoPositions =
|
||||||
|
i === request.body.secondary.length - 2 ||
|
||||||
|
i === request.body.secondary.length - 1;
|
||||||
|
|
||||||
|
newPass.secondaryFields.push({
|
||||||
|
key: `secondary${i}`,
|
||||||
|
label: field.label,
|
||||||
|
value: field.value,
|
||||||
|
textAlignment: isElementInLastTwoPositions
|
||||||
|
? "PKTextAlignmentRight"
|
||||||
|
: "PKTextAlignmentLeft",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(request.body.auxiliary)) {
|
||||||
|
for (let i = 0; i < request.body.auxiliary.length; i++) {
|
||||||
|
const field = request.body.auxiliary[i];
|
||||||
|
|
||||||
|
if (!(field?.label && field.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isElementInLastTwoPositions =
|
||||||
|
i === request.body.auxiliary.length - 2 ||
|
||||||
|
i === request.body.auxiliary.length - 1;
|
||||||
|
|
||||||
|
newPass.auxiliaryFields.push({
|
||||||
|
key: `auxiliary${i}`,
|
||||||
|
label: field.label,
|
||||||
|
value: field.value,
|
||||||
|
textAlignment: isElementInLastTwoPositions
|
||||||
|
? "PKTextAlignmentRight"
|
||||||
|
: "PKTextAlignmentLeft",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.body.qrText && request.body.codeType) {
|
||||||
|
newPass.setBarcodes({
|
||||||
|
message: request.body.qrText,
|
||||||
|
format: request.body.codeType,
|
||||||
|
messageEncoding: "iso-8859-1",
|
||||||
|
altText: request.body.codeAlt?.trim() ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { thumbnailFile, logoFile } = request.body;
|
||||||
|
|
||||||
|
// Downloading thumbnail and logo files from Firebase Storage and adding to pass
|
||||||
|
if (newPass.type == "generic" || newPass.type == "eventTicket") {
|
||||||
|
if (thumbnailFile) {
|
||||||
|
const tempPath1 = path.join(os.tmpdir(), thumbnailFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageRef
|
||||||
|
.file(`thumbnails/${thumbnailFile}`)
|
||||||
|
.download({ destination: tempPath1 });
|
||||||
|
|
||||||
|
const buffer = fs.readFileSync(tempPath1);
|
||||||
|
|
||||||
|
newPass.addBuffer("thumbnail.png", buffer);
|
||||||
|
newPass.addBuffer("thumbnail@2x.png", buffer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logoFile) {
|
||||||
|
const tempPath2 = path.join(os.tmpdir(), logoFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageRef
|
||||||
|
.file(`logos/${logoFile}`)
|
||||||
|
.download({ destination: tempPath2 });
|
||||||
|
|
||||||
|
const buffer = fs.readFileSync(tempPath2);
|
||||||
|
|
||||||
|
newPass.addBuffer("logo.png", buffer);
|
||||||
|
newPass.addBuffer("logo@2x.png", buffer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferData = newPass.getAsBuffer();
|
||||||
|
|
||||||
|
response.set("Content-Type", newPass.mimeType);
|
||||||
|
response.status(200).send(bufferData);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error Uploading pass " + error);
|
||||||
|
|
||||||
|
const err = Object.assign(
|
||||||
|
{},
|
||||||
|
...Object.entries(Object.getOwnPropertyDescriptors(error)).map(
|
||||||
|
([key, descriptor]) => {
|
||||||
|
return { [key]: descriptor.value };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
response.status(500);
|
||||||
|
response.send({
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
3
examples/firebase/functions/src/startData.json
Normal file
3
examples/firebase/functions/src/startData.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"FIREBASE_BUCKET_ADDR": "<insert your firebase storage bucket address (the part right after 'gs://...')"
|
||||||
|
}
|
||||||
30
examples/firebase/functions/tsconfig.json
Normal file
30
examples/firebase/functions/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"rootDir": "src",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "lib",
|
||||||
|
"importHelpers": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
12
examples/firebase/storage.rules
Normal file
12
examples/firebase/storage.rules
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
|
||||||
|
// Craft rules based on data in your Firestore database
|
||||||
|
// allow write: if firestore.get(
|
||||||
|
// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin;
|
||||||
|
service firebase.storage {
|
||||||
|
match /b/{bucket}/o {
|
||||||
|
match /{allPaths=**} {
|
||||||
|
allow read, write: if false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user