Initial commit
This commit is contained in:
commit
1b51a50c51
|
@ -0,0 +1,4 @@
|
|||
.git
|
||||
.env
|
||||
Dockerfile
|
||||
node_modules
|
|
@ -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
|
|
@ -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"]
|
|
@ -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
|
||||
```
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
return config;
|
||||
},
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import 'tailwindcss/tailwind.css'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
export default MyApp
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
}/>
|
||||
}/>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
} />
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
}/>
|
||||
}/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 858 B |
|
@ -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 |
|
@ -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')
|
||||
}
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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: [],
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue