Merge pull request #14 from covidpass-org/dev

Merge development version into main
This commit is contained in:
Sören Busse 2021-06-28 23:50:53 +02:00 committed by GitHub
commit 742e1c4b31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 155 additions and 105 deletions

49
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,49 @@
# This is a basic workflow to help you get started with Actions
name: Publish
on:
push:
branches:
- '**'
tags:
- 'v*.*.*'
pull_request:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: ghcr.io/covidpass-org/covidpass
# generate Docker tags based on the following events/attributes
tags: |
type=schedule
type=edge,branch=dev
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -9,9 +9,9 @@ export function Alert() {
} }
return( return(
<div id="alert" style={{"display": "none"}} className="bg-red-100 border border-red-400 text-red-700 px-4 mx-4 py-3 rounded relative" role="alert"> <div id="alert" style={{"display": "none"}} className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mt-5 rounded relative" role="alert">
<strong className="font-bold pr-2" id="heading"></strong> <strong className="font-bold pr-2" id="heading"/>
<span className="block sm:inline" id="message"></span> <span className="block sm:inline" id="message"/>
<span className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={close}> <span className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={close}>
<svg className="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><title>Close</title><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg> <svg className="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><title>Close</title><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg>
</span> </span>

View File

@ -2,10 +2,10 @@ const PDFJS = require('pdfjs-dist')
PDFJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS.version}/pdf.worker.js` PDFJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS.version}/pdf.worker.js`
import jsQR from "jsqr" import jsQR from "jsqr"
import { saveAs } from 'file-saver' import {saveAs} from 'file-saver'
import { decodeData } from "../src/decode" import {decodeData} from "../src/decode"
import { createPass } from "../src/pass" import {createPass} from "../src/pass"
import Card from "../components/Card" import Card from "../components/Card"
import Alert from "../components/Alert" import Alert from "../components/Alert"
@ -43,12 +43,12 @@ function Form() {
const file = document.getElementById('pdf').files[0] const file = document.getElementById('pdf').files[0]
const result = await readFileAsync(file) const result = await readFileAsync(file)
var typedarray = new Uint8Array(result) let typedArray = new Uint8Array(result)
const canvas = document.getElementById('canvas') const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
var loadingTask = PDFJS.getDocument(typedarray) let loadingTask = PDFJS.getDocument(typedArray)
await loadingTask.promise.then(async function (pdfDocument) { await loadingTask.promise.then(async function (pdfDocument) {
const pdfPage = await pdfDocument.getPage(1) const pdfPage = await pdfDocument.getPage(1)
const viewport = pdfPage.getViewport({ scale: 1 }) const viewport = pdfPage.getViewport({ scale: 1 })
@ -63,8 +63,8 @@ function Form() {
return await renderTask.promise return await renderTask.promise
}) })
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
var code = jsQR(imageData.data, imageData.width, imageData.height, { let code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert', inversionAttempts: 'dontInvert',
}) })
@ -78,9 +78,7 @@ function Form() {
error('Invalid QR code found', 'Make sure that you picked the correct PDF') error('Invalid QR code found', 'Make sure that you picked the correct PDF')
} }
const result = { decoded: decoded, raw: rawData } return {decoded: decoded, raw: rawData}
return result
} else { } else {
error('No QR code found', 'Try scanning the PDF again') error('No QR code found', 'Try scanning the PDF again')
} }
@ -119,7 +117,7 @@ function Form() {
saveAs(passBlob, 'covid.pkpass') saveAs(passBlob, 'covid.pkpass')
} }
} catch (e) { } catch (e) {
error('Error:', error.message) error('Error:', e.message)
} finally { } finally {
document.getElementById('spin').style.display = 'none' document.getElementById('spin').style.display = 'none'
} }
@ -155,7 +153,7 @@ function Form() {
<option value="teal">teal</option> <option value="teal">teal</option>
</select> </select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none"> <div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-5 h-5 fill-current" viewBox="0 0 20 20"><path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" fillRule="evenodd"></path></svg> <svg className="w-5 h-5 fill-current" viewBox="0 0 20 20"><path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" fillRule="evenodd"/></svg>
</div> </div>
</div> </div>
} /> } />
@ -177,8 +175,8 @@ function Form() {
</button> </button>
<div id="spin" style={{ "display": "none" }}> <div id="spin" style={{ "display": "none" }}>
<svg className="animate-spin h-5 w-5 ml-2" viewBox="0 0 24 24"> <svg className="animate-spin h-5 w-5 ml-2" viewBox="0 0 24 24">
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg> </svg>
</div> </div>
</div> </div>

4
pages/api/config.js Normal file
View File

@ -0,0 +1,4 @@
export default function handler(req, res) {
// Return the API_BASE_URL. This Endpoint allows us to access the env Variable in client javascript
res.status(200).json({ apiBaseUrl: process.env.API_BASE_URL })
}

View File

@ -1,7 +1,6 @@
import { NextSeo } from 'next-seo'; import { NextSeo } from 'next-seo';
import Form from '../components/Form' import Form from '../components/Form'
import Logo from '../components/Logo'
import Card from '../components/Card' import Card from '../components/Card'
import Page from '../components/Page' import Page from '../components/Page'

View File

@ -1,18 +1,19 @@
exports.BASE_URL = 'https://raw.githubusercontent.com/ehn-dcc-development/ehn-dcc-valuesets/main/' exports.VALUE_SET_BASE_URL = 'https://raw.githubusercontent.com/ehn-dcc-development/ehn-dcc-valuesets/main/'
exports.API_BASE_URL = 'https://api.covidpass.marvinsextro.de/'
exports.VALUE_TYPES = { exports.VALUE_TYPES = {
medicalProducts: 'vaccine-medicinal-product.json', medicalProducts: 'vaccine-medicinal-product.json',
countryCodes: 'country-2-codes.json', countryCodes: 'country-2-codes.json',
manufacturers: 'vaccine-mah-manf.json', manufacturers: 'vaccine-mah-manf.json',
} }
exports.COLORS = { exports.COLORS = {
white: 'rgb(255, 255, 255)', white: 'rgb(255, 255, 255)',
black: 'rgb(0, 0, 0)', black: 'rgb(0, 0, 0)',
grey: 'rgb(33, 33, 33)', grey: 'rgb(33, 33, 33)',
green: 'rgb(27, 94, 32)', green: 'rgb(27, 94, 32)',
indigo: 'rgb(26, 35, 126)', indigo: 'rgb(26, 35, 126)',
blue: 'rgb(1, 87, 155)', blue: 'rgb(1, 87, 155)',
purple: 'rgb(74, 20, 140)', purple: 'rgb(74, 20, 140)',
teal: 'rgb(0, 77, 64)', teal: 'rgb(0, 77, 64)',
} }
exports.NAME = 'CovidPass' exports.NAME = 'CovidPass'
exports.PASS_IDENTIFIER = 'pass.de.marvinsextro.covidpass' // WELL KNOWN
exports.TEAM_IDENTIFIER = 'X8Q7Q2RLTD' // WELL KNOWN

View File

@ -1,41 +1,37 @@
'use strict'; 'use strict';
const consts = require('./constants') const constants = require('./constants')
const utils = require('./utils') const utils = require('./utils')
const img = require('./img')
const { Payload } = require('./payload') const { Payload } = require('./payload')
const { toBuffer } = require('do-not-zip') const { toBuffer } = require('do-not-zip')
const crypto = require('crypto') const crypto = require('crypto')
exports.createPass = async function(data) { exports.createPass = async function(data) {
async function getJSONfromURL(url) {
return await (await fetch(url)).json()
}
function getBufferHash(buffer) { function getBufferHash(buffer) {
// creating hash // creating hash
const sha = crypto.createHash('sha1'); const sha = crypto.createHash('sha1');
sha.update(buffer); sha.update(buffer);
return sha.digest('hex'); return sha.digest('hex');
} }
async function signPassWithRemote(pass, payload) { async function signPassWithRemote(pass, payload) {
// From pass-js // From pass-js
// https://github.com/walletpass/pass-js/blob/2b6475749582ca3ea742a91466303cb0eb01a13a/src/pass.ts // https://github.com/walletpass/pass-js/blob/2b6475749582ca3ea742a91466303cb0eb01a13a/src/pass.ts
// Creating new Zip file // Creating new Zip file
const zip = [] const zip = []
// Adding required files // Adding required files
// Create pass.json // Create pass.json
zip.push({ path: 'pass.json', data: Buffer.from(JSON.stringify(pass)) }) zip.push({ path: 'pass.json', data: Buffer.from(JSON.stringify(pass)) })
const passHash = getBufferHash(Buffer.from(JSON.stringify(pass)))
zip.push({ path: 'icon.png', data: payload.img1x }) zip.push({ path: 'icon.png', data: payload.img1x })
zip.push({ path: 'icon@2x.png', data: payload.img2x }) zip.push({ path: 'icon@2x.png', data: payload.img2x })
zip.push({ path: 'logo.png', data: payload.img1x }) zip.push({ path: 'logo.png', data: payload.img1x })
zip.push({ path: 'logo@2x.png', data: payload.img2x }) zip.push({ path: 'logo@2x.png', data: payload.img2x })
// adding manifest // adding manifest
// Construct manifest here // Construct manifest here
const manifestJson = JSON.stringify( const manifestJson = JSON.stringify(
@ -48,30 +44,35 @@ exports.createPass = async function(data) {
), ),
); );
zip.push({ path: 'manifest.json', data: manifestJson }); zip.push({ path: 'manifest.json', data: manifestJson });
const response = await fetch(consts.API_BASE_URL + 'sign_manifest', { // Load API_BASE_URL form nextjs backend
const configResponse = await fetch('/api/config')
const apiBaseUrl = (await configResponse.json()).apiBaseUrl
const response = await fetch(`${apiBaseUrl}/sign`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/octet-stream', 'Accept': 'application/octet-stream',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
manifest: manifestJson PassJsonHash: passHash,
useBlackVersion: !payload.dark
}) })
}) })
if (response.status != 200) { if (response.status !== 200) {
return undefined return undefined
} }
const manifestSignature = await response.arrayBuffer() const manifestSignature = await response.arrayBuffer()
zip.push({ path: 'signature', data: Buffer.from(manifestSignature) }); zip.push({ path: 'signature', data: Buffer.from(manifestSignature) });
// finished! // finished!
return toBuffer(zip); return toBuffer(zip);
} }
let valueSets let valueSets
try { try {
@ -87,8 +88,6 @@ exports.createPass = async function(data) {
} catch (e) { } catch (e) {
return undefined return undefined
} }
let signingIdentity = await getJSONfromURL(consts.API_BASE_URL + 'signing_identity')
const qrCode = { const qrCode = {
message: payload.raw, message: payload.raw,
@ -97,14 +96,14 @@ exports.createPass = async function(data) {
} }
const pass = { const pass = {
passTypeIdentifier: signingIdentity['pass_identifier'], passTypeIdentifier: constants.PASS_IDENTIFIER,
teamIdentifier: signingIdentity['pass_team_id'], teamIdentifier: constants.TEAM_IDENTIFIER,
sharingProhibited: true, sharingProhibited: true,
voided: false, voided: false,
formatVersion: 1, formatVersion: 1,
logoText: consts.NAME, logoText: constants.NAME,
organizationName: consts.NAME, organizationName: constants.NAME,
description: consts.NAME, description: constants.NAME,
labelColor: payload.labelColor, labelColor: payload.labelColor,
foregroundColor: payload.foregroundColor, foregroundColor: payload.foregroundColor,
backgroundColor: payload.backgroundColor, backgroundColor: payload.backgroundColor,
@ -113,75 +112,74 @@ exports.createPass = async function(data) {
barcode: qrCode, barcode: qrCode,
generic: { generic: {
headerFields: [ headerFields: [
{ {
key: "type", key: "type",
label: "Certificate Type", label: "Certificate Type",
value: payload.certificateType value: payload.certificateType
} }
], ],
primaryFields: [ primaryFields: [
{ {
key: "name", key: "name",
label: "Name", label: "Name",
value: payload.name value: payload.name
} }
], ],
secondaryFields: [ secondaryFields: [
{ {
key: "dose", key: "dose",
label: "Dose", label: "Dose",
value: payload.dose value: payload.dose
}, },
{ {
key: "dov", key: "dov",
label: "Date of Vaccination", label: "Date of Vaccination",
value: payload.dateOfVaccination, value: payload.dateOfVaccination,
textAlignment: "PKTextAlignmentRight" textAlignment: "PKTextAlignmentRight"
} }
], ],
auxiliaryFields: [ auxiliaryFields: [
{ {
key: "vaccine", key: "vaccine",
label: "Vaccine", label: "Vaccine",
value: payload.vaccineName value: payload.vaccineName
}, },
{ {
key: "dob", key: "dob",
label: "Date of Birth", value: label: "Date of Birth", value:
payload.dateOfBirth, payload.dateOfBirth,
textAlignment: "PKTextAlignmentRight" textAlignment: "PKTextAlignmentRight"
} }
], ],
backFields: [ backFields: [
{ {
key: "uvci", key: "uvci",
label: "Unique Certificate Identifier (UVCI)", label: "Unique Certificate Identifier (UVCI)",
value: payload.uvci value: payload.uvci
}, },
{ {
key: "issuer", key: "issuer",
label: "Certificate Issuer", label: "Certificate Issuer",
value: payload.certificateIssuer value: payload.certificateIssuer
}, },
{ {
key: "country", key: "country",
label: "Country of Vaccination", label: "Country of Vaccination",
value: payload.countryOfVaccination value: payload.countryOfVaccination
}, },
{ {
key: "manufacturer", key: "manufacturer",
label: "Manufacturer", label: "Manufacturer",
value: payload.manufacturer value: payload.manufacturer
}, },
{ {
key: "disclaimer", key: "disclaimer",
label: "Disclaimer", label: "Disclaimer",
value: "This certificate is only valid in combination with the ID card of the certificate holder and expires one year + 14 days after the last dose. The validity of this certificate was not checked by CovidPass." value: "This certificate is only valid in combination with the ID card of the certificate holder and expires one year + 14 days after the last dose. The validity of this certificate was not checked by CovidPass."
} }
] ]
} }
}; };
let buf = await signPassWithRemote(pass, payload) return await signPassWithRemote(pass, payload)
return buf
} }

View File

@ -83,6 +83,7 @@ exports.Payload = class {
this.img2x = img2x this.img2x = img2x
this.raw = rawData this.raw = rawData
this.dark = dark
this.name = name this.name = name
this.dose = dose this.dose = dose

View File

@ -1,15 +1,15 @@
const { VALUE_TYPES, BASE_URL } = require("./constants") const { VALUE_TYPES, VALUE_SET_BASE_URL } = require("./constants")
const fetch = require("node-fetch") const fetch = require("node-fetch")
exports.getValueSets = async function() { exports.getValueSets = async function() {
async function getJSONfromURL(url) { async function getJSONFromURL(url) {
return await (await fetch(url)).json() return await (await fetch(url)).json()
} }
var valueSets = {} let valueSets = {}
for (const [key, value] of Object.entries(VALUE_TYPES)) { for (const [key, value] of Object.entries(VALUE_TYPES)) {
valueSets[key] = await getJSONfromURL(BASE_URL + value) valueSets[key] = await getJSONFromURL(VALUE_SET_BASE_URL + value)
} }
return valueSets return valueSets