Initial commit

This commit is contained in:
Marvin Sextro 2021-06-25 12:18:25 +02:00
commit 1b51a50c51
27 changed files with 13588 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.git
.env
Dockerfile
node_modules

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

42
Dockerfile Normal file
View File

@ -0,0 +1,42 @@
# Install dependencies only when needed
FROM node:14-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json ./
RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed
FROM node:14-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build
# Production image, copy all the files and run next
FROM node:14-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/server.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
ENV NEXT_TELEMETRY_DISABLED 1
CMD ["yarn", "start"]

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# CovidPass
Webtool for generating a wallet pass from an official EU COVID-19 Vaccination Certificate QR code
## Debug locally
```sh
docker build . -t covidpass
docker run --env-file .env -t -i -p 3000:3000 covidpass
```

23
components/Card.js Normal file
View File

@ -0,0 +1,23 @@
export default Card
function Card({heading, content, step}) {
return (
<div className="rounded-3xl shadow-xl p-2 m-4">
{ step ?
<div className="flex flex-row items-center p-2">
<div className="rounded-full p-4 bg-green-600 h-5 w-5 flex items-center justify-center">
<p className="text-white text-lg font-bold">
{step}
</p>
</div>
<div className="pl-3 font-bold text-xl">
{heading}
</div>
</div> :
<p></p>}
<div className="p-4 text-lg">
{content}
</div>
</div>
)
}

107
components/Form.js Normal file
View File

@ -0,0 +1,107 @@
const PDFJS = require('pdfjs-dist')
PDFJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS.version}/pdf.worker.js`
import jsQR from "jsqr";
import { decodeData } from "../src/decode"
import Card from "../components/Card"
export default Form
function Form() {
const processPdf = async function(input) {
const file = input.target.files[0]
var fileReader = new FileReader();
fileReader.onload = async function() {
var typedarray = new Uint8Array(this.result)
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
var loadingTask = PDFJS.getDocument(typedarray)
await loadingTask.promise.then(async function (pdfDocument) {
const pdfPage = await pdfDocument.getPage(1)
const viewport = pdfPage.getViewport({ scale: 1 })
canvas.width = viewport.width
canvas.height = viewport.height
const renderTask = pdfPage.render({
canvasContext: ctx,
viewport,
})
return await renderTask.promise
})
console.log(canvas.toDataURL('image/png'));
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert',
})
if (code) {
const rawData = code.data
console.log(rawData)
const decoded = decodeData(rawData)
console.log(decoded)
const result = JSON.stringify({ decoded: decoded, raw: rawData })
const payload = document.getElementById('payload')
payload.setAttribute('value', result)
const download = document.getElementById('download')
download.disabled = false
}
}
fileReader.readAsArrayBuffer(file)
}
return (
<div>
<form id="form">
<Card step={1} heading="Select Certificate" content={
<div className="space-y-5">
<p>
Please select the (scanned) certificate PDF, which you received from your doctor, pharmacy, vaccination centre or online.
</p>
<input
type="file"
id="pdf"
accept="application/pdf"
required
onChange={(input) => { processPdf(input) }}
style={{
"whiteSpace": "nowrap",
"overflow": "hidden",
"width": "300px",
}}
/>
</div>
}/>
<Card step={2} heading="Add to Wallet" content={
<div className="space-y-5">
<label htmlFor="privacy" className="flex flex-row space-x-4">
<input type="checkbox" id="privacy" value="privacy" required />
<p>
I have read the <a href="/privacy">Privacy Policy</a>
</p>
</label>
<form id="hidden" action="/api/covid.pkpass" method="POST">
<input type="hidden" id="payload" name="payload" />
<button id="download" type="download" disabled className="shadow-inner focus:outline-none bg-green-600 py-1 px-2 text-white font-semibold rounded-md disabled:bg-gray-400">
Add to Wallet
</button>
</form>
</div>
}/>
<canvas id="canvas" style={{display: "none"}} />
</form>
</div>
)
}

14
components/Logo.js Normal file
View File

@ -0,0 +1,14 @@
import Icon from '../public/favicon.svg'
export default Logo
function Logo() {
return (
<div className="flex flex-row items-center p-3 justify-center space-x-1" >
<Icon />
<h1 className="text-3xl font-bold">
CovidPass
</h1>
</div>
)
}

15
components/Page.js Normal file
View File

@ -0,0 +1,15 @@
import Head from 'next/head'
export default Page
function Page({content}) {
return (
<div className="lg:w-1/3 lg:mx-auto flex flex-col min-h-screen justify-center md:px-12">
<Head>
<title>CovidPass</title>
<link rel="icon" href="/favicon.png" />
</Head>
{content}
</div>
)
}

37
docker-compose.yml Normal file
View File

@ -0,0 +1,37 @@
version: "3.3"
services:
traefik:
image: "traefik:v2.4"
command:
#- "--log.level=DEBUG"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
#- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.myresolver.acme.email=marvin.sextro@gmail.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
- "8080:8080"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
covidpass-api:
container_name: covidpass-api
image: "marvinsxtr/covidpass-api"
restart: "unless-stopped"
environment:
- NODE_ENV=production
ports:
- "8000:8000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.covidpass-api.rule=Host(`api.covidpass.marvinsextro.de`)"
- "traefik.http.routers.covidpass-api.entrypoints=websecure"
- "traefik.http.routers.covidpass-api.tls.certresolver=myresolver"

9
next.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
}

12901
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "covidpass",
"version": "0.1.0",
"author": "Marvin Sextro <marvin.sextro@gmail.com>",
"private": true,
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"@walletpass/pass-js": "^6.9.1",
"base45-js": "^1.0.1",
"cbor-js": "^0.1.0",
"file-saver": "^2.0.5",
"jsqr": "^1.4.0",
"next": "latest",
"pdfjs-dist": "^2.5.207",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"worker-loader": "^3.0.7",
"webpack": "^5.0.0"
},
"devDependencies": {
"@svgr/webpack": "^5.5.0",
"autoprefixer": "^10.0.4",
"postcss": "^8.1.10",
"tailwindcss": "^2.1.1"
}
}

7
pages/_app.js Normal file
View File

@ -0,0 +1,7 @@
import 'tailwindcss/tailwind.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp

77
pages/api/covid.pkpass.js Normal file
View File

@ -0,0 +1,77 @@
import { img1xblack48dp, img2xblack48dp } from '../../res/img'
import { getValueSets } from '../../src/util'
import { SECRETS } from '../../src/constants'
const { Template, constants } = require("@walletpass/pass-js")
module.exports = async (req, res) => {
let data = JSON.parse(JSON.parse(JSON.stringify(req.body))["payload"])
const valueSets = await getValueSets()
let raw = data.raw
let decoded = data.decoded
const template = new Template("generic", {
passTypeIdentifier: SECRETS.PASS_TYPE_ID,
teamIdentifier: SECRETS.TEAM_ID,
sharingProhibited: true,
voided: false,
formatVersion: 1,
logoText: "CovidPass",
organizationName: "CovidPass",
description: "CovidPass",
labelColor: "rgb(0, 0, 0)",
foregroundColor: "rgb(0, 0, 0)",
backgroundColor: "rgb(255, 255, 255)",
})
await template.images.add("icon", img1xblack48dp, '1x')
await template.images.add("icon", img2xblack48dp, '2x')
await template.images.add("logo", img1xblack48dp, '1x')
await template.images.add("logo", img2xblack48dp, '2x')
template.setCertificate(SECRETS.CERT, SECRETS.PASSPHRASE)
const qrCode = {
"message": raw,
"format": "PKBarcodeFormatQR",
"messageEncoding": "utf-8"
}
const v = decoded["-260"]["1"]["v"][0]
const nam = decoded["-260"]["1"]["nam"]
const dob = decoded["-260"]["1"]["dob"]
const pass = template.createPass({
serialNumber: v["ci"],
barcodes: [qrCode],
barcode: qrCode
});
const vaccine_name = valueSets.vaccine_medical_products["valueSetValues"][v["mp"]]["display"]
const vaccine_prophylaxis = valueSets.vaccine_prophylaxis["valueSetValues"][v["vp"]]["display"]
const country_of_vaccination = valueSets.country_codes["valueSetValues"][v["co"]]["display"]
const marketing_auth_holder = valueSets.marketing_auth_holders["valueSetValues"][v["ma"]]["display"]
pass.headerFields.add({ key: "type", label: "Certificate Type", value: "Vaccination" })
pass.primaryFields.add({ key: "vaccine", label: "Vaccine", value: vaccine_name })
pass.secondaryFields.add({ key: "name", label: "Name", value: nam["fn"] + ', ' + nam["gn"] })
pass.secondaryFields.add({ key: "dob", label: "Date of Birth", value: dob, textAlignment: constants.textDirection.RIGHT })
pass.auxiliaryFields.add({ key: "dose", label: "Dose", value: v["dn"] + '/' + v["sd"] })
pass.auxiliaryFields.add({ key: "dov", label: "Date of Vaccination", value: v["dt"], textAlignment: constants.textDirection.RIGHT })
pass.backFields.add({ key: "uvci", label: "Unique Certificate Identifier (UVCI)", value: v["ci"]})
pass.backFields.add({ key: "issuer", label: "Certificate Issuer", value: v["is"] })
pass.backFields.add({ key: "cov", label: "Country of Vaccination", value: country_of_vaccination})
pass.backFields.add({ key: "vp", label: "Vaccine Prophylaxis", value: vaccine_prophylaxis })
pass.backFields.add({ key: "ma", label: "Marketing Authorization Holder", value: marketing_auth_holder })
const buf = await pass.asBuffer();
res.type = 'application/vnd.apple.pkpass'
res.body = buf
res.status(200).send(buf)
}

48
pages/imprint.js Normal file
View File

@ -0,0 +1,48 @@
import Page from '../components/Page'
import Card from '../components/Card'
export default function Imprint() {
return(
<Page content={
<Card step=" " heading="Impressum" content={
<p className="space-y-2">
<p className="font-bold">Information according to § 5 TMG</p>
<p>
Marvin Sextro<br />
Wilhelm-Busch-Str. 8A<br />
30167 Hannover<br />
</p>
<p className="font-bold">Contact</p>
<p>
marvin.sextro@gmail.com
</p>
<p className="font-bold">EU Dispute Resolution</p>
<p>
The European Commission provides a platform for online dispute resolution (OS): https://ec.europa.eu/consumers/odr. You can find our e-mail address in the imprint above.
</p>
<p className="font-bold">Consumer dispute resolution / universal arbitration board</p>
<p>
We are not willing or obliged to participate in dispute resolution proceedings before a consumer arbitration board.
</p>
<p className="font-bold">Liability for contents</p>
<p>
As a service provider, we are responsible for our own content on these pages in accordance with § 7 paragraph 1 TMG under the general laws. According to §§ 8 to 10 TMG, we are not obligated to monitor transmitted or stored information or to investigate circumstances that indicate illegal activity. Obligations to remove or block the use of information under the general laws remain unaffected. However, liability in this regard is only possible from the point in time at which a concrete infringement of the law becomes known. If we become aware of any such infringements, we will remove the relevant content immediately.
</p>
<p className="font-bold">Liability for links</p>
<p>
Our offer contains links to external websites of third parties, on whose contents we have no influence. Therefore, we cannot assume any liability for these external contents. The respective provider or operator of the sites is always responsible for the content of the linked sites. The linked pages were checked for possible legal violations at the time of linking. Illegal contents were not recognizable at the time of linking. However, a permanent control of the contents of the linked pages is not reasonable without concrete evidence of a violation of the law. If we become aware of any infringements, we will remove such links immediately.
</p>
<p className="font-bold">Copyright</p>
<p>
The contents and works created by the site operators on these pages are subject to German copyright law. Duplication, processing, distribution, or any form of commercialization of such material beyond the scope of the copyright law shall require the prior written consent of its respective author or creator. Downloads and copies of this site are only permitted for private, non-commercial use. Insofar as the content on this site was not created by the operator, the copyrights of third parties are respected. In particular, third-party content is identified as such. Should you nevertheless become aware of a copyright infringement, please inform us accordingly. If we become aware of any infringements, we will remove such content immediately.
</p>
<p className="font-bold">Credits</p>
<p>
Source: https://www.e-recht24.de/impressum-generator.html
Translated with www.DeepL.com/Translator (free version)
</p>
</p>
}/>
}/>
)
}

31
pages/index.js Normal file
View File

@ -0,0 +1,31 @@
import Form from '../components/Form'
import Logo from '../components/Logo'
import Card from '../components/Card'
import Page from '../components/Page'
export default function Home() {
return (
<Page content={
<div>
<main className="flex flex-col">
<Logo />
<Card content={
<p>
Convert any EU Digital Covid Certificate into a pass in your wallet app. On iOS, please use the Safari Browser.
</p>
} />
<Form className="flex-grow" />
<footer>
<nav className="nav flex space-x-5 m-6 flex-row-reverse space-x-reverse text-md font-bold">
<a href="/privacy" className="hover:underline" >Privacy Policy</a>
<a href="/imprint" className="hover:underline" >Imprint</a>
</nav>
</footer>
</main>
</div>
} />
)
}

14
pages/privacy.js Normal file
View File

@ -0,0 +1,14 @@
import Page from '../components/Page'
import Card from '../components/Card'
export default function Privacy() {
return(
<Page content={
<Card step=" " heading="Privacy Policy" content={
<p className="space-y-2">
Privacy Policy
</p>
}/>
}/>
)
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

1
public/favicon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/></g><g><path d="M11.3,2.26l-6,2.25C4.52,4.81,4,5.55,4,6.39v4.71c0,5.05,3.41,9.76,8,10.91c4.59-1.15,8-5.86,8-10.91V6.39 c0-0.83-0.52-1.58-1.3-1.87l-6-2.25C12.25,2.09,11.75,2.09,11.3,2.26z M10.23,14.83l-2.12-2.12c-0.39-0.39-0.39-1.02,0-1.41l0,0 c0.39-0.39,1.02-0.39,1.41,0l1.41,1.41l3.54-3.54c0.39-0.39,1.02-0.39,1.41,0l0,0c0.39,0.39,0.39,1.02,0,1.41l-4.24,4.24 C11.26,15.22,10.62,15.22,10.23,14.83z"/></g></svg>

After

Width:  |  Height:  |  Size: 588 B

6
res/img.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
img1xblack48dp: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABrUlEQVR4Ae3SA6iecRSA8dlWbra9tKXZ3nKzkXEzZy9jCnOYbZth5rWNc59rf/rr4n3ql+ucU6eel5eXkbpgPU7gKFajA6p1jTEX55EBKScNpzADjVBtGo69iIAE6B92YJDLF9mK1xBFz7AeHey/iALbLzYTPyGWfMM0aGkCMiGWZWA8lLsFceQmlEuEOJIA5cSlWnqAd4B3QByeI6MmHnAD7ZHXaKTWpANuoAVKd9H5AQrLN8RnhwcoLE90EOKbemkQHzahHSbhbxDLH4D4xmwN/YJU4RZK1xd/NS0PZmvopc/XoCqOUF0ezNbQFUgVcrAA5euhYXkwW0M7IT5kYC7Kp7o8mK2hGZDgj1BeHszWUFtkBX+E8vJZYLaeHkMCkIGlyKsxDkNCw0yNLYcE4T3+QBQwU1/NEQ2xJBrM1Nt2iCXM0l9nREEMiwKzzLQCYhYzDFYf9yCG3AMzzNYd4RDNwtEdVhqHFIgmKRgHqy1AJkRRJhbASZMRr7B8PCbDaQPxFRKkrxiIalF77ENmgC+zD+1R7RqAy5AqXMYAVPu6IQyfCoWhGzTn5eWVC6Pwjab6QBMNAAAAAElFTkSuQmCC', 'base64'),
img2xblack48dp: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAADIUlEQVR4Ae3aA6xeWxCG4XtwbRdxatu2bcWpGbthbdu2bdsIatvG9K2NX5219543eWJ1vp51+I1HsizLsizLsizL+hWV0RRNUB4/wYph36EaZuM+5B13MAFlEA8rSmVDb1yAfKGT6Ig0CCPrf7TDLkiENqAJ/kAE2RMThjCeKHtiIqDwRNkTo/NE2ROj8ETZE+P8E2VPjD1RyTES9yE+cwcD8D+crCguQ3zuAgrCqXLgNiQgbiErnOg7HIQEzHbEQb1GkICqCPXmQQJqKtQ7CwmovVDvISSg7kE9CTYbwAYINhvABgg2G8AGCDgbwAbQZwN4xHJ0x1RcsAG+ntMoiTf7F/NsgNg7jdT4UL/iqA2gcnwiam8DaB2fqL4NoHd8+whQPr59DlA8fgImQWwAneOPg0ROv0eQCNxHe2TCvyiLLR45/iOodyPCf0BxvFsihjp9fOCG1/8qYiQ+VjyGOnt84CzUOwwJUyNQGCNoHx84DPW2QsLUABTyCPrHB7ZCvamQMA0FhTSCG8cHpkK9rpAwPUS+EEbo7Mzxga5QrwUkAheQCRGkcHygBdQrAlEYQff4QBGo9yseKYygffxH+BVOtA+iMILO8YF9cKbhEIURdI4PDIcz1YIojKBzfKAWnOkPPFAYQev4D/AHnGolJMYj6B8fWAnnagmJgQsojzf7B7MgSlrCuf7GXUiMrEFPTMV5iJK7+BtONgXic1PgbGUgPlcGzhaH3RCf2o04OF0DiE81gPMl4jDEZw4jEZ6ogf3v1y0OmyA+sQlx8FT58BjicY+RD55sKMTjhsKz/YZjEI86ht/g6UrisUefnpLwRV0gHtMFvikByyAesQwJ8FX/4CjEcUfxD3xZWlyAOOoC0sLX5cR1iGOuIycCURHcgDjiBoogUOXGRYiyi8iNQJYWxyFKjiMtAl0SrFA4/gokAVkJ6ITHkBh7jE5IwDtZpXEEEiNHUBqfyPoFPfEQEiUP0RO/4AuzMmEOJEJzkAlWmOXDQkiIFiIfopSVEt1wCfIRl9ANKRGjrO9RCv1x/IX+KIXv4XSWZVmWZVmWZVlPAET6QNBhhZWqAAAAAElFTkSuQmCC', 'base64'),
img1xwhite48dp: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAABs0lEQVRYw+2WsUsCURzHn1RDLWZgc7UI2dIQONVWEDYILs0GDULQ7l9gDUXRHxA0tTRFDeXgIhTSENhkUGQYCllgotGnxRS78957d2cQ+B3v3u/zgd/de+8nRD//KIwT54hD1hlzGz1EhBPq/KTGMWEG3YHPssMLZnlmixlnLdnkBlmuiGu3zNASWfRaxgoP2Mk9yyr4BRrYTZ15uSCFk1zKBe+OBG9ygcP0BX8qeOXauBndE1zgE4I5PnojuGCkuf60F4I2foA79wUtvBDs63+D2q+KDUZZpGCK3zOeq3LBY0dBqvk00FRY4+FRLsh2tqP1PEBBioesXHDeUfBFtPVmSoqHc7lg23DGRwxr9rr+ANtyQdjkGoko4iEsF3j5tFJY4j/xqlyaGdPLcFUIhjiw3CEZtUs/1qX8lifJFoypCYYp2zqpywyrDi5JW4Kk+uDlp6SNL+HXme3WtAVresOjh7QWPo1Hdz6dpKiMLzIp9EOIqhK+SkjYC1GFObXRPg7tKJaoWOIrLAlnIUi+Kz5PUDgPPnZNWtVgF59wK0xz1oE/Y1q4HSZIkCNHggnRzz/KN71MawEqqdc4AAAAAElFTkSuQmCC', 'base64'),
img2xwhite48dp: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAADKElEQVR4Ae3aM6BsZxSG4XUQ22pj27ZttTH7Il1s27Zt22iubRtPynCCOXff/c/Mevrqe2fWMFJKKaWUUkqpFWAZHIYzcDoOwpKRqoNFcSSew2x/NQMPY390R1owsCWuwxj/3VBcgvUj/X9YDRfiW333MU7H8tEHeWKalyeqghPTSJ6oGk5MnqhCTkyeqOpPTJ6okk9MniishXswW/uZgZuxWpQIe2C89jcGu0RJsDWm6xzTsEVJ725+0Xm+QlfUDafqXIdE3fCizvVE1A0jda4fom6Yq3PNirrpcBkgA3S0DJABOlsGyAAdLgNkgPJlgHK8havwBMZkgIVnOPaJ38EqeDEDVG841mv8J18DM0Dl4zeGizJA5eM3hpMyQPXj5zOgsPHzNaCA8XvwKGSAesZ/ENohwDx9MxsXYVOsggPwOeWPj3lRN0zRvHnYK/4Evbij8PFhSqv/K+KeaADduKPg8WFk1A39Ne/UaKxxhDLGh/5RN3yheSdHY40jlDE+fBF1wxOad0c01ihCKeMX88+4KzRvLnb8HxEuK2h8uCLqhrP1zRhsGn1Uw/hwdtQNu0P1EYobH3aPumEZzKs+QnHjz8MyUQL8CNVHKGZ8+DFKgbug+gjFjA93RSlwLFQfoZjx4dgoBZbHnOojFDP+HCwfJcE7UHWEAsaHd6I0OEc1xuCg+B2sjGfV55woDVbCTNV5H9fgCYxWn5lYKUqEx7W/x6NU2F/72z9KhS58p319h64oGU7Wvk6O0qEX/bWf/uiNVoCT89FfI3ThU+3jU3RFK8GOmK/1zceO0Ypwh9Z3R7QqLItBWtcgLButDPtgvtYzH/tEO8DlWs/l0S7Qgze1jjfRE+0EK2Og8g3EytGOsAHGKNcYbBDtDNtgsvJMxjbRCbA7pijHFOwenQTbYaz6jcV20YmwAQarz2BsEJ0Mq+NtC9/bWD1SBHpwKear3nxcip5If4T9MEB1BmC/aCxhaVyDuRacubgGS0f6b7Apntd3z2PTaE7CjnjF//cKdowFI2EdXIlxGhuHK7FOVCNhMeyLmzAYg3ET9sVikVJKKaWUUkqF+xW+vCJnJJaqGwAAAABJRU5ErkJggg==', 'base64')
}

25
server.js Normal file
View File

@ -0,0 +1,25 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
if (pathname === '/a') {
app.render(req, res, '/a', query)
} else if (pathname === '/b') {
app.render(req, res, '/b', query)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

15
src/constants.js Normal file
View File

@ -0,0 +1,15 @@
export const BASE_URL = 'https://raw.githubusercontent.com/ehn-dcc-development/ehn-dcc-valuesets/main/'
export const VALUE_TYPES = {
vaccine_medical_products: 'vaccine-medicinal-product.json',
country_codes: 'country-2-codes.json',
vaccine_auth_holders: 'vaccine-mah-manf.json',
vaccine_prophylaxis: 'vaccine-prophylaxis.json',
marketing_auth_holders: 'vaccine-mah-manf.json'
}
export const PASS_MIME_TYPE = 'application/vnd.apple.pkpass'
export const SECRETS = {
TEAM_ID: process.env.TEAM_ID,
PASS_TYPE_ID: process.env.PASS_TYPE_ID,
CERT: Buffer.from(process.env.CERT, 'base64').toString('ascii'),
PASSPHRASE: process.env.PASSPHRASE,
}

69
src/decode.js Normal file
View File

@ -0,0 +1,69 @@
// Taken from https://github.com/ehn-dcc-development/ehn-sign-verify-javascript-trivial/blob/main/cose_verify.js
// and https://github.com/ehn-dcc-development/dgc-check-mobile-app/blob/2c2ebf4e9b7650ceef44f7e1fb05a57572830c5b/src/app/cose-js/sign.js
const base45 = require('base45-js')
const zlib = require('pako')
const cbor = require('cbor-js')
export function typedArrayToBufferSliced(array) {
return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset)
}
export function typedArrayToBuffer(array) {
var buffer = new ArrayBuffer(array.length)
array.map(function(value, i) {
buffer[i] = value
})
return array.buffer
}
export function toBuffer(ab) {
var buf = Buffer.alloc(ab.byteLength)
var view = new Uint8Array(ab)
for (var i = 0; i < buf.length; ++i) {
buf[i] = view[i]
}
return buf
}
export function decodeData(data) {
console.log(data)
data = data.toString('ASCII')
if (data.startsWith('HC1')) {
data = data.substring(3)
if (data.startsWith(':')) {
data = data.substring(1)
} else {
console.log("Warning: unsafe HC1: header - update to v0.0.4")
}
} else {
console.log("Warning: no HC1: header - update to v0.0.4")
}
data = base45.decode(data)
if (data[0] == 0x78) {
data = zlib.inflate(data)
}
data = cbor.decode(typedArrayToBuffer(data));
if (!Array.isArray(data)) {
throw new Error('Expecting Array')
}
if (data.length !== 4) {
throw new Error('Expecting Array of length 4')
}
let plaintext = data[2]
let decoded = cbor.decode(typedArrayToBufferSliced(plaintext))
console.log(JSON.stringify(decoded, null, 4))
return decoded
}

15
src/util.js Normal file
View File

@ -0,0 +1,15 @@
import { VALUE_TYPES, BASE_URL } from "./constants"
export async function getValueSets() {
var valueSets = {}
for (const [key, value] of Object.entries(VALUE_TYPES)) {
valueSets[key] = await getJSONfromURL(BASE_URL + value)
}
return valueSets
}
export async function getJSONfromURL(url) {
return await (await fetch(url)).json()
}

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false,
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

38
traefik.yml Normal file
View File

@ -0,0 +1,38 @@
# Configuration for Traefik v2.
global:
checkNewVersion: true
sendAnonymousUsage: false
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
log:
level: DEBUG
filePath: log/traefik.log
api:
dashboard: true
providers:
docker:
certificatesResolvers:
myresolver:
acme:
email: "marvin.sextro@gmail.com"
storage: "acme.json"
# Staging: "https://acme-staging-v02.api.letsencrypt.org/directory"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlsChallenge:
httpChallenge:
entryPoint: web