Merge pull request #153 from Saim-Khan1/master

Added firebase example
This commit is contained in:
Alexander Cerutti
2023-07-30 16:51:15 +02:00
committed by GitHub
12 changed files with 14927 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"projects": {
"default": "<put here your project name>"
}
}

63
examples/firebase/.gitignore vendored Normal file
View 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

File diff suppressed because one or more lines are too long

View 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"
}
}

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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
}

View 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,
});
}
},
);

View File

@@ -0,0 +1,3 @@
{
"FIREBASE_BUCKET_ADDR": "<insert your firebase storage bucket address (the part right after 'gs://...')"
}

View 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
}
}

View 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;
}
}
}