Merge pull request #24 from covidpass-org/typescript
Transition to Typescript
This commit is contained in:
commit
d141fa43e1
|
@ -30,3 +30,6 @@ yarn-error.log*
|
|||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Idea files
|
||||
.idea
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
export default Alert
|
||||
|
||||
export function Alert() {
|
||||
|
||||
const close = function() {
|
||||
const alert = document.getElementById('alert')
|
||||
alert.setAttribute('style', 'display: none;')
|
||||
}
|
||||
|
||||
return(
|
||||
<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"/>
|
||||
<span className="block sm:inline" id="message"/>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
interface AlertProps {
|
||||
onClose: () => void;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
function Alert(props: AlertProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div 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">Error</strong>
|
||||
<span className="block sm:inline" id="message">{props.errorMessage}</span>
|
||||
<span className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={props.onClose}>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Alert;
|
|
@ -1,24 +1,30 @@
|
|||
export default Card
|
||||
interface CardProps {
|
||||
heading?: string,
|
||||
step?: string,
|
||||
content: JSX.Element,
|
||||
}
|
||||
|
||||
function Card({heading, content, step}) {
|
||||
function Card(props: CardProps): JSX.Element {
|
||||
return (
|
||||
<div className="rounded-md p-6 bg-white dark:bg-gray-800 space-y-4">
|
||||
{
|
||||
step &&
|
||||
props.step &&
|
||||
<div className="flex flex-row items-center">
|
||||
<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}
|
||||
{props.step}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-3 font-bold text-xl">
|
||||
{heading}
|
||||
{props.heading}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="text-lg">
|
||||
{content}
|
||||
{props.content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Card;
|
|
@ -1,143 +1,32 @@
|
|||
import Card from "./Card";
|
||||
import {saveAs} from 'file-saver'
|
||||
import {BrowserQRCodeReader} from '@zxing/browser'
|
||||
import React, {useEffect, useRef, useState} from "react"
|
||||
import {decodeData} from "../src/decode"
|
||||
import {processPdf, processPng} from "../src/process"
|
||||
import {createPass} from "../src/pass"
|
||||
import Card from "../components/Card"
|
||||
import Alert from "../components/Alert"
|
||||
import jsQR from "jsqr";
|
||||
import React, {FormEvent, useEffect, useRef, useState} from "react";
|
||||
import {BrowserQRCodeReader} from "@zxing/browser";
|
||||
import {Result} from "@zxing/library";
|
||||
import {PayloadBody} from "../src/payload";
|
||||
import {getPayloadBodyFromFile, getPayloadBodyFromQR} from "../src/process";
|
||||
import {PassData} from "../src/pass";
|
||||
import Alert from "./Alert";
|
||||
|
||||
export default Form
|
||||
function Form(): JSX.Element {
|
||||
|
||||
function Form() {
|
||||
// Whether camera is open or not
|
||||
const [isCameraOpen, setIsCameraOpen] = useState<boolean>(false);
|
||||
|
||||
function readFileAsync(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
})
|
||||
}
|
||||
|
||||
const error = function (heading, message) {
|
||||
const alert = document.getElementById('alert')
|
||||
alert.setAttribute('style', null)
|
||||
|
||||
document.getElementById('heading').innerHTML = heading
|
||||
document.getElementById('message').innerHTML = message
|
||||
|
||||
document.getElementById('spin').style.display = 'none'
|
||||
}
|
||||
|
||||
const processFile = async function () {
|
||||
console.log(qrCode)
|
||||
console.log(file)
|
||||
if (!qrCode && !file) {
|
||||
error("Error", "Please capture a QR Code or select a file to scan");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('spin').style.display = 'block'
|
||||
|
||||
let rawData;
|
||||
|
||||
if (file) {
|
||||
let imageData
|
||||
const fileBuffer = await readFileAsync(file)
|
||||
|
||||
switch (file.type) {
|
||||
case 'application/pdf':
|
||||
console.log('pdf')
|
||||
imageData = await processPdf(fileBuffer)
|
||||
break
|
||||
case 'image/png':
|
||||
console.log('png')
|
||||
imageData = await processPng(fileBuffer)
|
||||
break
|
||||
default:
|
||||
error('Error', 'Invalid file type')
|
||||
return
|
||||
}
|
||||
|
||||
let code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: 'dontInvert',
|
||||
})
|
||||
|
||||
rawData = code.data;
|
||||
|
||||
} else {
|
||||
rawData = qrCode.getText()
|
||||
}
|
||||
|
||||
if (rawData) {
|
||||
let decoded
|
||||
|
||||
try {
|
||||
decoded = decodeData(rawData)
|
||||
} catch (e) {
|
||||
error('Invalid QR code found', 'Try another method to select your certificate')
|
||||
return;
|
||||
}
|
||||
|
||||
return {decoded: decoded, raw: rawData}
|
||||
} else {
|
||||
error('No QR code found', 'Try another method to select your certificate')
|
||||
}
|
||||
}
|
||||
|
||||
const addToWallet = async function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
let result
|
||||
|
||||
try {
|
||||
result = await processFile()
|
||||
} catch (e) {
|
||||
error('Error:', 'Could not extract QR code data from certificate')
|
||||
}
|
||||
|
||||
if (typeof result === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const color = document.getElementById('color').value
|
||||
|
||||
try {
|
||||
const pass = await createPass(
|
||||
{
|
||||
decoded: result.decoded,
|
||||
raw: result.raw,
|
||||
color: color
|
||||
}
|
||||
)
|
||||
|
||||
if (!pass) {
|
||||
error('Error:', "Something went wrong.")
|
||||
} else {
|
||||
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
|
||||
saveAs(passBlob, 'covid.pkpass')
|
||||
}
|
||||
} catch (e) {
|
||||
error('Error:', e.message)
|
||||
} finally {
|
||||
document.getElementById('spin').style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
||||
// Global camera controls
|
||||
const [globalControls, setGlobalControls] = useState(undefined);
|
||||
const [qrCode, setQrCode] = useState(undefined);
|
||||
const [file, setFile] = useState(undefined);
|
||||
|
||||
const inputFile = useRef(undefined)
|
||||
// Currently selected QR Code / File. Only one of them is set.
|
||||
const [qrCode, setQrCode] = useState<Result>(undefined);
|
||||
const [file, setFile] = useState<File>(undefined);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string>(undefined);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
// File Input ref
|
||||
const inputFile = useRef<HTMLInputElement>(undefined)
|
||||
|
||||
// Add event listener to listen for file change events
|
||||
useEffect(() => {
|
||||
if (inputFile && inputFile.current) {
|
||||
inputFile.current.addEventListener('input', () => {
|
||||
|
@ -150,12 +39,12 @@ function Form() {
|
|||
}
|
||||
}, [inputFile])
|
||||
|
||||
// Show file Dialog
|
||||
async function showFileDialog() {
|
||||
inputFile.current.click();
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Hide camera view
|
||||
async function hideCameraView() {
|
||||
if (globalControls !== undefined) {
|
||||
globalControls.stop();
|
||||
|
@ -163,36 +52,76 @@ function Form() {
|
|||
setIsCameraOpen(false);
|
||||
}
|
||||
|
||||
// Show camera view
|
||||
async function showCameraView() {
|
||||
// Create new QR Code Reader
|
||||
const codeReader = new BrowserQRCodeReader();
|
||||
|
||||
// Needs to be called before any camera can be accessed
|
||||
await BrowserQRCodeReader.listVideoInputDevices();
|
||||
|
||||
// Get preview Element to show camera stream
|
||||
const previewElem = document.querySelector('#cameraPreview');
|
||||
const previewElem: HTMLVideoElement = document.querySelector('#cameraPreview');
|
||||
|
||||
setGlobalControls(await codeReader.decodeFromVideoDevice(undefined, previewElem, (result, error, controls) => {
|
||||
// Set Global controls
|
||||
setGlobalControls(
|
||||
// Start decoding from video device
|
||||
await codeReader.decodeFromVideoDevice(undefined,
|
||||
previewElem,
|
||||
(result, error, controls) => {
|
||||
if (result !== undefined) {
|
||||
setQrCode(result);
|
||||
setFile(undefined);
|
||||
|
||||
if (result !== undefined) {
|
||||
setQrCode(result);
|
||||
setFile(undefined);
|
||||
controls.stop();
|
||||
|
||||
controls.stop();
|
||||
|
||||
// Reset
|
||||
setGlobalControls(undefined);
|
||||
setIsCameraOpen(false);
|
||||
}
|
||||
}));
|
||||
// Reset
|
||||
setGlobalControls(undefined);
|
||||
setIsCameraOpen(false);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
setIsCameraOpen(true);
|
||||
}
|
||||
|
||||
// Add Pass to wallet
|
||||
async function addToWallet(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (!file && !qrCode) {
|
||||
setErrorMessage("Please scan a QR Code, or select a file to scan")
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const color = (document.getElementById('color') as HTMLSelectElement).value;
|
||||
let payloadBody: PayloadBody;
|
||||
|
||||
try {
|
||||
if (file) {
|
||||
payloadBody = await getPayloadBodyFromFile(file, color);
|
||||
} else {
|
||||
payloadBody = await getPayloadBodyFromQR(qrCode, color);
|
||||
}
|
||||
|
||||
let pass = await PassData.generatePass(payloadBody);
|
||||
|
||||
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
|
||||
saveAs(passBlob, 'covid.pkpass');
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setErrorMessage(e.toString());
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className="space-y-5" id="form" onSubmit={(e) => addToWallet(e)}>
|
||||
<Card step={1} heading="Select Certificate" content={
|
||||
<form className="space-y-5" id="form" onSubmit={addToWallet}>
|
||||
<Card step="1" heading="Select Certificate" content={
|
||||
<div className="space-y-5">
|
||||
<p>
|
||||
Please select the certificate screenshot or (scanned) PDF page, which you received from your
|
||||
|
@ -214,7 +143,8 @@ function Form() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<video id="cameraPreview" className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
|
||||
<video id="cameraPreview"
|
||||
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
|
||||
<input type='file'
|
||||
id='file'
|
||||
accept="application/pdf,image/png"
|
||||
|
@ -228,7 +158,7 @@ function Form() {
|
|||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<span className="w-full">
|
||||
<span className="w-full truncate">
|
||||
{
|
||||
qrCode && 'Found QR Code!'
|
||||
}
|
||||
|
@ -236,11 +166,11 @@ function Form() {
|
|||
file && file.name
|
||||
}
|
||||
</span>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}/>
|
||||
<Card step={2} heading="Pick a Color" content={
|
||||
<Card step="2" heading="Pick a Color" content={
|
||||
<div className="space-y-5">
|
||||
<p>
|
||||
Pick a background color for your pass.
|
||||
|
@ -267,7 +197,7 @@ function Form() {
|
|||
</div>
|
||||
</div>
|
||||
}/>
|
||||
<Card step={3} heading="Add to Wallet" content={
|
||||
<Card step="3" heading="Add to Wallet" content={
|
||||
<div className="space-y-5">
|
||||
<p>
|
||||
Data privacy is of special importance when processing health-related data.
|
||||
|
@ -281,11 +211,11 @@ function Form() {
|
|||
</p>
|
||||
</label>
|
||||
<div className="flex flex-row items-center justify-start">
|
||||
<button id="download" type="download"
|
||||
<button id="download" type="submit"
|
||||
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
|
||||
Add to Wallet
|
||||
</button>
|
||||
<div id="spin" style={{"display": "none"}}>
|
||||
<div id="spin" className={loading ? undefined : "hidden"}>
|
||||
<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"/>
|
||||
|
@ -297,8 +227,12 @@ function Form() {
|
|||
</div>
|
||||
}/>
|
||||
</form>
|
||||
<Alert/>
|
||||
<canvas id="canvas" style={{display: "none"}}/>
|
||||
{
|
||||
errorMessage && <Alert errorMessage={errorMessage} onClose={() => setErrorMessage(undefined)}/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form;
|
|
@ -1,8 +1,6 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default Logo
|
||||
|
||||
function Logo() {
|
||||
function Logo(): JSX.Element {
|
||||
return (
|
||||
<Link href="/">
|
||||
<a className="flex flex-row items-center p-3 justify-center space-x-1">
|
||||
|
@ -23,4 +21,6 @@ function Logo() {
|
|||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Logo
|
|
@ -1,32 +0,0 @@
|
|||
import Head from 'next/head'
|
||||
import Logo from './Logo'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default Page
|
||||
|
||||
function Page({ content }) {
|
||||
return (
|
||||
<div className="md:w-2/3 xl:w-2/5 md:mx-auto flex flex-col min-h-screen justify-center px-5 py-12">
|
||||
<Head>
|
||||
<title>CovidPass</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div>
|
||||
<main className="flex flex-col space-y-5">
|
||||
<Logo />
|
||||
|
||||
{content}
|
||||
|
||||
<footer>
|
||||
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold">
|
||||
<a href="https://www.paypal.com/paypalme/msextro" className="hover:underline" >Donate</a>
|
||||
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline" >GitHub</a>
|
||||
<Link href="/privacy"><a className="hover:underline" >Privacy Policy</a></Link>
|
||||
<Link href="/imprint"><a className="hover:underline" >Imprint</a></Link>
|
||||
</nav>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import Head from 'next/head'
|
||||
import Logo from './Logo'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface PageProps {
|
||||
content: JSX.Element
|
||||
}
|
||||
|
||||
function Page(props: PageProps): JSX.Element {
|
||||
return (
|
||||
<div className="md:w-2/3 xl:w-2/5 md:mx-auto flex flex-col min-h-screen justify-center px-5 py-12">
|
||||
<Head>
|
||||
<title>CovidPass</title>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
</Head>
|
||||
<div>
|
||||
<main className="flex flex-col space-y-5">
|
||||
<Logo/>
|
||||
|
||||
{props.content}
|
||||
|
||||
<footer>
|
||||
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold">
|
||||
<a href="https://www.paypal.com/paypalme/msextro" className="hover:underline">Donate</a>
|
||||
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline">GitHub</a>
|
||||
<Link href="/privacy"><a className="hover:underline">Privacy Policy</a></Link>
|
||||
<Link href="/imprint"><a className="hover:underline">Imprint</a></Link>
|
||||
</nav>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
|
@ -0,0 +1,3 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
|
@ -9,7 +9,7 @@
|
|||
"dependencies": {
|
||||
"@zxing/browser": "^0.0.9",
|
||||
"@zxing/library": "^0.18.6",
|
||||
"base45-js": "^1.0.1",
|
||||
"base45": "^3.0.0",
|
||||
"cbor-js": "^0.1.0",
|
||||
"do-not-zip": "^1.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
|
@ -22,13 +22,17 @@
|
|||
"pngjs": "^6.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
"webpack": "^5.0.0",
|
||||
"worker-loader": "^3.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pako": "^1.0.1",
|
||||
"@types/react": "^17.0.11",
|
||||
"autoprefixer": "^10.0.4",
|
||||
"postcss": "^8.1.10",
|
||||
"tailwindcss": "^2.1.1"
|
||||
"tailwindcss": "^2.1.1",
|
||||
"typescript": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
@ -290,12 +294,41 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz",
|
||||
"integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA=="
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.1.tgz",
|
||||
"integrity": "sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "17.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz",
|
||||
"integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz",
|
||||
"integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz",
|
||||
|
@ -649,13 +682,10 @@
|
|||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base45-js": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base45-js/-/base45-js-1.0.1.tgz",
|
||||
"integrity": "sha512-AQW3SAO/VVaof+gE/QoQLstyzFRIuQZOwkAX9AY/Ur+qBkNqUfsv8h77vBqsiAgCrTBNBWU+c5m5B0QgKmAT+Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
"node_modules/base45": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base45/-/base45-3.0.0.tgz",
|
||||
"integrity": "sha512-SWayzDiUFaga+G3X/oL2XIDbjhdpmBGUqLU6T7zB89aNWYnEcfa36DIWW95rxn0sWAWbwrKcSyqcWw6AApYwsg=="
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
|
@ -1150,6 +1180,12 @@
|
|||
"postcss": "^8.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
||||
|
@ -4079,6 +4115,19 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
||||
|
@ -4169,6 +4218,14 @@
|
|||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vm-browserify": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||
|
@ -4624,12 +4681,41 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz",
|
||||
"integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA=="
|
||||
},
|
||||
"@types/pako": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.1.tgz",
|
||||
"integrity": "sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "17.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz",
|
||||
"integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@types/scheduler": {
|
||||
"version": "0.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz",
|
||||
"integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==",
|
||||
"dev": true
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz",
|
||||
|
@ -4931,10 +5017,10 @@
|
|||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"base45-js": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base45-js/-/base45-js-1.0.1.tgz",
|
||||
"integrity": "sha512-AQW3SAO/VVaof+gE/QoQLstyzFRIuQZOwkAX9AY/Ur+qBkNqUfsv8h77vBqsiAgCrTBNBWU+c5m5B0QgKmAT+Q=="
|
||||
"base45": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base45/-/base45-3.0.0.tgz",
|
||||
"integrity": "sha512-SWayzDiUFaga+G3X/oL2XIDbjhdpmBGUqLU6T7zB89aNWYnEcfa36DIWW95rxn0sWAWbwrKcSyqcWw6AApYwsg=="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
|
@ -5352,6 +5438,12 @@
|
|||
"cssnano-preset-simple": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
|
||||
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
|
||||
"dev": true
|
||||
},
|
||||
"data-uri-to-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
||||
|
@ -7569,6 +7661,12 @@
|
|||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
|
||||
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz",
|
||||
"integrity": "sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==",
|
||||
"dev": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
||||
|
@ -7646,6 +7744,11 @@
|
|||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"vm-browserify": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
"dependencies": {
|
||||
"@zxing/browser": "^0.0.9",
|
||||
"@zxing/library": "^0.18.6",
|
||||
"base45-js": "^1.0.1",
|
||||
"base45": "^3.0.0",
|
||||
"cbor-js": "^0.1.0",
|
||||
"do-not-zip": "^1.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
|
@ -24,12 +24,16 @@
|
|||
"pngjs": "^6.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
"webpack": "^5.0.0",
|
||||
"worker-loader": "^3.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pako": "^1.0.1",
|
||||
"@types/react": "^17.0.11",
|
||||
"autoprefixer": "^10.0.4",
|
||||
"postcss": "^8.1.10",
|
||||
"tailwindcss": "^2.1.1"
|
||||
"tailwindcss": "^2.1.1",
|
||||
"typescript": "^4.3.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import 'tailwindcss/tailwind.css'
|
||||
|
||||
import App from 'next/app';
|
||||
import { DefaultSeo } from 'next-seo';
|
||||
|
||||
import SEO from '../next-seo.config';
|
||||
|
||||
export default class MyApp extends App {
|
||||
render() {
|
||||
const { Component, pageProps } = this.props;
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo {...SEO} />
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import 'tailwindcss/tailwind.css'
|
||||
|
||||
import {DefaultSeo} from 'next-seo';
|
||||
import SEO from '../next-seo.config';
|
||||
import type {AppProps} from 'next/app'
|
||||
|
||||
function MyApp({Component, pageProps}: AppProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo {...SEO} />
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp;
|
|
@ -1,7 +1,7 @@
|
|||
import Document, {Html, Head, Main, NextScript} from 'next/document'
|
||||
|
||||
class CustomDocument extends Document {
|
||||
render() {
|
||||
class MyDocument extends Document {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head/>
|
||||
|
@ -14,4 +14,4 @@ class CustomDocument extends Document {
|
|||
}
|
||||
}
|
||||
|
||||
export default CustomDocument
|
||||
export default MyDocument;
|
|
@ -1,4 +0,0 @@
|
|||
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 })
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
|
||||
type ConfigData = {
|
||||
apiBaseUrl: string
|
||||
}
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse<ConfigData>) {
|
||||
// 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})
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import Page from '../components/Page'
|
||||
import Card from '../components/Card'
|
||||
|
||||
export default function Imprint() {
|
||||
return(
|
||||
<Page content={
|
||||
<Card step="§" heading="Imprint" content={
|
||||
<div 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): <a href="https://ec.europa.eu/consumers/odr" className="underline">https://ec.europa.eu/consumers/odr</a>. 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">Credits</p>
|
||||
<p>
|
||||
With excerpts from: https://www.e-recht24.de/impressum-generator.html
|
||||
Translated with www.DeepL.com/Translator (free version)
|
||||
</p>
|
||||
</div>
|
||||
}/>
|
||||
}/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import Page from '../components/Page'
|
||||
import Card from '../components/Card'
|
||||
|
||||
function Imprint(): JSX.Element {
|
||||
return (
|
||||
<Page content={
|
||||
<Card step="§" heading="Imprint" content={
|
||||
<div 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): <a
|
||||
href="https://ec.europa.eu/consumers/odr"
|
||||
className="underline">https://ec.europa.eu/consumers/odr</a>. 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">Credits</p>
|
||||
<p>
|
||||
With excerpts from: https://www.e-recht24.de/impressum-generator.html
|
||||
Translated with www.DeepL.com/Translator (free version)
|
||||
</p>
|
||||
</div>
|
||||
}/>
|
||||
}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Imprint;
|
|
@ -1,46 +0,0 @@
|
|||
import { NextSeo } from 'next-seo';
|
||||
|
||||
import Form from '../components/Form'
|
||||
import Card from '../components/Card'
|
||||
import Page from '../components/Page'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title="Covidpass"
|
||||
description="Add your EU Digital Covid Vaccination Certificates to your favorite wallet app."
|
||||
openGraph={{
|
||||
url: 'https://covidpass.marvinsextro.de/',
|
||||
title: 'CovidPass',
|
||||
description: 'Add your EU Digital Covid Vaccination Certificates to your favorite wallet app.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://covidpass.marvinsextro.de/thumbnail.png',
|
||||
width: 1000,
|
||||
height: 500,
|
||||
alt: 'CovidPass: Add your EU Digital Covid Vaccination Certificates to your favorite wallet app.',
|
||||
}
|
||||
],
|
||||
site_name: 'CovidPass',
|
||||
}}
|
||||
twitter={{
|
||||
handle: '@marvinsxtr',
|
||||
site: '@marvinsxtr',
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
/>
|
||||
<Page content={
|
||||
<div className="space-y-5">
|
||||
<Card content={
|
||||
<p>
|
||||
Add your EU Digital Covid Vaccination Certificates to your favorite wallet app. On iOS, please use the Safari Browser.
|
||||
</p>
|
||||
} />
|
||||
|
||||
<Form className="flex-grow" />
|
||||
</div>
|
||||
} />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {NextSeo} from 'next-seo';
|
||||
|
||||
import Form from '../components/Form'
|
||||
import Card from '../components/Card'
|
||||
import Page from '../components/Page'
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title="Covidpass"
|
||||
description="Add your EU Digital Covid Vaccination Certificates to your favorite wallet app."
|
||||
openGraph={{
|
||||
url: 'https://covidpass.marvinsextro.de/',
|
||||
title: 'CovidPass',
|
||||
description: 'Add your EU Digital Covid Vaccination Certificates to your favorite wallet app.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://covidpass.marvinsextro.de/thumbnail.png',
|
||||
width: 1000,
|
||||
height: 500,
|
||||
alt: 'CovidPass: Add your EU Digital Covid Vaccination Certificates to your favorite wallet app.',
|
||||
}
|
||||
],
|
||||
site_name: 'CovidPass',
|
||||
}}
|
||||
twitter={{
|
||||
handle: '@marvinsxtr',
|
||||
site: '@marvinsxtr',
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
/>
|
||||
<Page content={
|
||||
<div className="space-y-5">
|
||||
<Card content={
|
||||
<p>
|
||||
Add your EU Digital Covid Vaccination Certificates to your favorite wallet app. On iOS,
|
||||
please use the Safari Browser.
|
||||
</p>
|
||||
}/>
|
||||
|
||||
<Form/>
|
||||
</div>
|
||||
}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index;
|
201
pages/privacy.js
201
pages/privacy.js
|
@ -1,201 +0,0 @@
|
|||
import Page from '../components/Page'
|
||||
import Card from '../components/Card'
|
||||
|
||||
export default function Privacy() {
|
||||
return(
|
||||
<Page content={
|
||||
<Card step="i" heading="Privacy Policy" content={
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
Our privacy policy is based on the terms used by the European legislator for the adoption of the General Data Protection Regulation (GDPR).
|
||||
</p>
|
||||
<p className="font-bold">General information</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>
|
||||
The whole process of generating the pass file happens locally in your browser. For the signing step, only a hashed representation of your data is sent to the server.
|
||||
</li>
|
||||
<li>
|
||||
Your data is not stored beyond the active browser session and the site does not use cookies.
|
||||
</li>
|
||||
<li>
|
||||
No data is sent to third parties.
|
||||
</li>
|
||||
<li>
|
||||
We transmit your data securely over https.
|
||||
</li>
|
||||
<li>
|
||||
Our server is hosted in Nuremberg, Germany.
|
||||
</li>
|
||||
<li>
|
||||
The source code of this site is available on <a href="https://github.com/marvinsxtr/covidpass" className="underline">GitHub</a>.
|
||||
</li>
|
||||
<li>
|
||||
By default, Apple Wallet passes are accessible from the lock screen. This can be changed in the <a href="https://support.apple.com/de-de/guide/iphone/iph9a2a69136/ios" className="underline">settings</a>.
|
||||
</li>
|
||||
<li>
|
||||
The server provider processes data to provide this site. In order to better understand what measures they take to protect your data, please also read their <a href="https://www.hetzner.com/de/rechtliches/datenschutz/" className="underline">privacy policy</a> and the <a href="https://docs.hetzner.com/general/general-terms-and-conditions/data-privacy-faq/" className="underline">data privacy FAQ</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Contact</p>
|
||||
<p>
|
||||
Marvin Sextro<br />
|
||||
Wilhelm-Busch-Str. 8A<br />
|
||||
30167 Hannover<br />
|
||||
Germany<br />
|
||||
Email: marvin.sextro@gmail.com<br />
|
||||
Website: <a href="https://marvinsextro.de" className="underline">https://marvinsextro.de</a><br />
|
||||
</p>
|
||||
<p className="font-bold">Simplified explanation of the process</p>
|
||||
<p>
|
||||
This process is only started after accepting this policy and clicking on the Add to Wallet button.
|
||||
</p>
|
||||
<p>
|
||||
First, the following steps happen locally in your browser:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Recognizing and extracting the QR code data from your selected certificate</li>
|
||||
<li>Decoding your personal and health-related data from the QR code payload</li>
|
||||
<li>Assembling an incomplete pass file out of your data</li>
|
||||
<li>Generating a file containing hashes of the data stored in the pass file</li>
|
||||
<li>Sending only the file containing the hashes to our server</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Second, the following steps happen on our server:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Receiving and checking the hashes which were generated locally</li>
|
||||
<li>Signing the file containing the hashes</li>
|
||||
<li>Sending the signature back</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Finally, the following steps happen locally in your browser:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Assembling the signed pass file out of the incomplete file generated locally and the signature</li>
|
||||
<li>Saving the file on your device</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Locally processed data</p>
|
||||
<p>
|
||||
The following data is processed on in your browser to generate the pass file.
|
||||
</p>
|
||||
<p>
|
||||
Processed personal data contained in the QR code:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Your first and last name</li>
|
||||
<li>Your date of birth</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For each vaccination certificate contained in the QR code, the following data is processed:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Targeted disease</li>
|
||||
<li>Vaccine medical product</li>
|
||||
<li>Manufacturer/Marketing Authorization Holder</li>
|
||||
<li>Dose number</li>
|
||||
<li>Total series of doses</li>
|
||||
<li>Date of vaccination</li>
|
||||
<li>Country of vaccination</li>
|
||||
<li>Certificate issuer</li>
|
||||
<li>Unique certificate identifier (UVCI)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For each test certificate contained in the QR code, the following data is processed:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Targeted disease</li>
|
||||
<li>Test type</li>
|
||||
<li>NAA Test name</li>
|
||||
<li>RAT Test name and manufacturer</li>
|
||||
<li>Date/Time of Sample Collection</li>
|
||||
<li>Test Result</li>
|
||||
<li>Testing Centre</li>
|
||||
<li>Country of test</li>
|
||||
<li>Certificate Issuer</li>
|
||||
<li>Unique Certificate Identifier (UVCI)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For each recovery certificate contained in the QR code, the following data is processed:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Targeted disease</li>
|
||||
<li>Date of first positive NAA test result</li>
|
||||
<li>Country of test</li>
|
||||
<li>Certificate Issuer</li>
|
||||
<li>Certificate valid from</li>
|
||||
<li>Certificate valid until</li>
|
||||
<li>Unique Certificate Identifier (UVCI)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
The <a href="https://github.com/ehn-dcc-development/ehn-dcc-schema" className="underline">Digital Covid Certificate Schema</a> contains a detailed specification of which data can be contained in the QR code.
|
||||
</p>
|
||||
<p className="font-bold">Server provider</p>
|
||||
<p>
|
||||
Our server provider is <a href="https://www.hetzner.com/" className="underline">Hetzner Online GmbH</a>.
|
||||
The following data may be collected and stored in the server log files:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>The browser types and versions used</li>
|
||||
<li>The operating system used by the accessing system</li>
|
||||
<li>The website from which an accessing system reaches our website (so-called referrers)</li>
|
||||
<li>The date and time of access</li>
|
||||
<li>The pseudonymised IP addresses</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Your rights</p>
|
||||
In accordance with the GDPR you have the following rights:
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>
|
||||
Right of access to your data: You have the right to know what data has been collected about you and how it was processed.
|
||||
</li>
|
||||
<li>
|
||||
Right to be forgotten: Erasure of your personal data.
|
||||
</li>
|
||||
<li>
|
||||
Right of rectification: You have the right to correct inaccurate data.
|
||||
</li>
|
||||
<li>
|
||||
Right of data portability: You have the right to transfer your data from one processing system into another.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Third parties linked</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>
|
||||
GitHub: <a href="https://docs.github.com/en/github/site-policy/github-privacy-statement" className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
PayPal: <a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full?locale.x=en_EN" className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
Gmail/Google: <a href="https://policies.google.com/privacy?hl=en-US" className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
Apple may sync your passes via iCloud: <a href="https://www.apple.com/legal/privacy/en-ww/" className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}/>
|
||||
}/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
import Page from '../components/Page'
|
||||
import Card from '../components/Card'
|
||||
|
||||
function Privacy(): JSX.Element {
|
||||
return (
|
||||
<Page content={
|
||||
<Card step="i" heading="Privacy Policy" content={
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
Our privacy policy is based on the terms used by the European legislator for the adoption of the
|
||||
General Data Protection Regulation (GDPR).
|
||||
</p>
|
||||
<p className="font-bold">General information</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>
|
||||
The whole process of generating the pass file happens locally in your browser. For the
|
||||
signing step, only a hashed representation of your data is sent to the server.
|
||||
</li>
|
||||
<li>
|
||||
Your data is not stored beyond the active browser session and the site does not use
|
||||
cookies.
|
||||
</li>
|
||||
<li>
|
||||
No data is sent to third parties.
|
||||
</li>
|
||||
<li>
|
||||
We transmit your data securely over https.
|
||||
</li>
|
||||
<li>
|
||||
Our server is hosted in Nuremberg, Germany.
|
||||
</li>
|
||||
<li>
|
||||
The source code of this site is available on <a
|
||||
href="https://github.com/marvinsxtr/covidpass" className="underline">GitHub</a>.
|
||||
</li>
|
||||
<li>
|
||||
By default, Apple Wallet passes are accessible from the lock screen. This can be changed
|
||||
in the <a href="https://support.apple.com/de-de/guide/iphone/iph9a2a69136/ios"
|
||||
className="underline">settings</a>.
|
||||
</li>
|
||||
<li>
|
||||
The server provider processes data to provide this site. In order to better understand
|
||||
what measures they take to protect your data, please also read their <a
|
||||
href="https://www.hetzner.com/de/rechtliches/datenschutz/" className="underline">privacy
|
||||
policy</a> and the <a
|
||||
href="https://docs.hetzner.com/general/general-terms-and-conditions/data-privacy-faq/privacy.tsx"
|
||||
className="underline">data privacy FAQ</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Contact</p>
|
||||
<p>
|
||||
Marvin Sextro<br/>
|
||||
Wilhelm-Busch-Str. 8A<br/>
|
||||
30167 Hannover<br/>
|
||||
Germany<br/>
|
||||
Email: marvin.sextro@gmail.com<br/>
|
||||
Website: <a href="https://marvinsextro.de"
|
||||
className="underline">https://marvinsextro.de</a><br/>
|
||||
</p>
|
||||
<p className="font-bold">Simplified explanation of the process</p>
|
||||
<p>
|
||||
This process is only started after accepting this policy and clicking on the Add to Wallet
|
||||
button.
|
||||
</p>
|
||||
<p>
|
||||
First, the following steps happen locally in your browser:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Recognizing and extracting the QR code data from your selected certificate</li>
|
||||
<li>Decoding your personal and health-related data from the QR code payload</li>
|
||||
<li>Assembling an incomplete pass file out of your data</li>
|
||||
<li>Generating a file containing hashes of the data stored in the pass file</li>
|
||||
<li>Sending only the file containing the hashes to our server</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Second, the following steps happen on our server:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Receiving and checking the hashes which were generated locally</li>
|
||||
<li>Signing the file containing the hashes</li>
|
||||
<li>Sending the signature back</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Finally, the following steps happen locally in your browser:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Assembling the signed pass file out of the incomplete file generated locally and the
|
||||
signature
|
||||
</li>
|
||||
<li>Saving the file on your device</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Locally processed data</p>
|
||||
<p>
|
||||
The following data is processed on in your browser to generate the pass file.
|
||||
</p>
|
||||
<p>
|
||||
Processed personal data contained in the QR code:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Your first and last name</li>
|
||||
<li>Your date of birth</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For each vaccination certificate contained in the QR code, the following data is processed:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Targeted disease</li>
|
||||
<li>Vaccine medical product</li>
|
||||
<li>Manufacturer/Marketing Authorization Holder</li>
|
||||
<li>Dose number</li>
|
||||
<li>Total series of doses</li>
|
||||
<li>Date of vaccination</li>
|
||||
<li>Country of vaccination</li>
|
||||
<li>Certificate issuer</li>
|
||||
<li>Unique certificate identifier (UVCI)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For each test certificate contained in the QR code, the following data is processed:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Targeted disease</li>
|
||||
<li>Test type</li>
|
||||
<li>NAA Test name</li>
|
||||
<li>RAT Test name and manufacturer</li>
|
||||
<li>Date/Time of Sample Collection</li>
|
||||
<li>Test Result</li>
|
||||
<li>Testing Centre</li>
|
||||
<li>Country of test</li>
|
||||
<li>Certificate Issuer</li>
|
||||
<li>Unique Certificate Identifier (UVCI)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For each recovery certificate contained in the QR code, the following data is processed:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>Targeted disease</li>
|
||||
<li>Date of first positive NAA test result</li>
|
||||
<li>Country of test</li>
|
||||
<li>Certificate Issuer</li>
|
||||
<li>Certificate valid from</li>
|
||||
<li>Certificate valid until</li>
|
||||
<li>Unique Certificate Identifier (UVCI)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
The <a href="https://github.com/ehn-dcc-development/ehn-dcc-schema" className="underline">Digital
|
||||
Covid Certificate Schema</a> contains a detailed specification of which data can be contained in
|
||||
the QR code.
|
||||
</p>
|
||||
<p className="font-bold">Server provider</p>
|
||||
<p>
|
||||
Our server provider is <a href="https://www.hetzner.com/" className="underline">Hetzner Online
|
||||
GmbH</a>.
|
||||
The following data may be collected and stored in the server log files:
|
||||
</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>The browser types and versions used</li>
|
||||
<li>The operating system used by the accessing system</li>
|
||||
<li>The website from which an accessing system reaches our website (so-called referrers)
|
||||
</li>
|
||||
<li>The date and time of access</li>
|
||||
<li>The pseudonymised IP addresses</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Your rights</p>
|
||||
In accordance with the GDPR you have the following rights:
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>
|
||||
Right of access to your data: You have the right to know what data has been collected
|
||||
about you and how it was processed.
|
||||
</li>
|
||||
<li>
|
||||
Right to be forgotten: Erasure of your personal data.
|
||||
</li>
|
||||
<li>
|
||||
Right of rectification: You have the right to correct inaccurate data.
|
||||
</li>
|
||||
<li>
|
||||
Right of data portability: You have the right to transfer your data from one processing
|
||||
system into another.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p className="font-bold">Third parties linked</p>
|
||||
<div className="px-4">
|
||||
<ul className="list-disc">
|
||||
<li>
|
||||
GitHub: <a href="https://docs.github.com/en/github/site-policy/github-privacy-statement"
|
||||
className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
PayPal: <a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full?locale.x=en_EN"
|
||||
className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
Gmail/Google: <a href="https://policies.google.com/privacy?hl=en-US"
|
||||
className="underline">Privacy Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
Apple may sync your passes via iCloud: <a
|
||||
href="https://www.apple.com/legal/privacy/en-ww/privacy.tsx" className="underline">Privacy
|
||||
Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}/>
|
||||
}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Privacy;
|
|
@ -1,19 +0,0 @@
|
|||
exports.VALUE_SET_BASE_URL = 'https://raw.githubusercontent.com/ehn-dcc-development/ehn-dcc-valuesets/main/'
|
||||
exports.VALUE_TYPES = {
|
||||
medicalProducts: 'vaccine-medicinal-product.json',
|
||||
countryCodes: 'country-2-codes.json',
|
||||
manufacturers: 'vaccine-mah-manf.json',
|
||||
}
|
||||
exports.COLORS = {
|
||||
white: 'rgb(255, 255, 255)',
|
||||
black: 'rgb(0, 0, 0)',
|
||||
grey: 'rgb(33, 33, 33)',
|
||||
green: 'rgb(27, 94, 32)',
|
||||
indigo: 'rgb(26, 35, 126)',
|
||||
blue: 'rgb(1, 87, 155)',
|
||||
purple: 'rgb(74, 20, 140)',
|
||||
teal: 'rgb(0, 77, 64)',
|
||||
}
|
||||
exports.NAME = 'CovidPass'
|
||||
exports.PASS_IDENTIFIER = 'pass.de.marvinsextro.covidpass' // WELL KNOWN
|
||||
exports.TEAM_IDENTIFIER = 'X8Q7Q2RLTD' // WELL KNOWN
|
|
@ -0,0 +1,21 @@
|
|||
export class Constants {
|
||||
public static NAME = 'CovidPass'
|
||||
public static PASS_IDENTIFIER = 'pass.de.marvinsextro.covidpass'
|
||||
public static TEAM_IDENTIFIER = 'X8Q7Q2RLTD'
|
||||
|
||||
public static COLORS = {
|
||||
white: 'rgb(255, 255, 255)',
|
||||
black: 'rgb(0, 0, 0)',
|
||||
grey: 'rgb(33, 33, 33)',
|
||||
green: 'rgb(27, 94, 32)',
|
||||
indigo: 'rgb(26, 35, 126)',
|
||||
blue: 'rgb(1, 87, 155)',
|
||||
purple: 'rgb(74, 20, 140)',
|
||||
teal: 'rgb(0, 77, 64)',
|
||||
}
|
||||
|
||||
public static img1xBlack: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAABU0lEQVR4AWIYaWAUiAExoB06gGggDOMwripJthAgGwBoCGYDBIDIgAFBwDAABoJjhBgEAEChBBBaAA0JyeKAqR0hmnWx3p5o8MHdvfd9Z7SHH8Dr723iCpdoYBOZtoJ9XOALYghxjj0sw1k7OEEAiekVxyjBShto4h6SUg8N5KGqhCHEshdsI3FdiCM3SNwnxA1uKxKXZm3QfJCPQ3RmYVAfW5j2YH+QfkweQ1uDviEmdNHBR8SYddxCDOC2ojeI4RlL+K2Kd8UYcFvRE8TQxyKmVdFLOAbcVnQNMeEUCzCKPQbcVnQEiRilGQNuK9qFRI1SjAG3Fa0iiDh8hgPcQWIKwG1dHsQyD+qKCCGWhCgiVZ7T7yhagw9JyQe37FTGCKI0QhlWq2GiGDNBDU6qYwyJaYw6nFbBABJhgAoyKYc2QoghRBs5ZF4BLTz+aaGAef+nHwt5/579e2c2AAAAAElFTkSuQmCC', 'base64');
|
||||
public static img2xBlack: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACZ0lEQVR4Ae3TA8xcURRF4dq2ozqqbRtxUjeq7Zi127C2bXvSsLZtu/PP6ardjs577765K/li7mQnizGbzWaz2Wx50BXjMRYdkR0JXRq0xVq8g/ziDZaiGVIiYSqLybgPCdMtjEZpP1+oP45CYnQYPZDd7xeKnPMX1L+QAoULmnUhX12wVrwupHjBKnC8tFgEMcRcpIFjrYQYZhkcqQXEUM2h3haIoTZDvRsQQ92AeiGIoUJQTwxnB7ID2YHsQHYgO5B7zmMlztiBfhbCCKQAJUuOYbADIYRe+FP7TB1IfxyiUaYOpD8O0TJzB9IfpyqCZg6kP05ZPIBESL0gJAIBVENONMRJF8cJQr1nkDBdRWb8WBYEHB8HeAb1bkPCNB5E/xlJfRzgNtQ7CQnTWNB/R9IfBzgJ9TZCwnQJGcMYSX8cYCPUmw6JwCqkwt9K5cg4wHSo1x0SoZX/GUJ/HKA71KsAURhJdxygAtRLg1cKI2mP8wpp4EibIQoj6YwDbIZj9YIojKQzDtALjlUESZAYrEN2fK2u4jhJKAJH2wmJ0UOsRQBJECU74XjtIYZoD8dLi1sQj7uFtHClIRCPGwLXyox7EI+6h8xwtR4Qj+oB10uFExCPOYFU8ERVEIR4RBBV4KlGQTxiFDxXWgQgLgsgLTxZQdyBuOQOCsLTVcELiMNeoAqMqBHeQhzyFo1gVC3wCqLsFVrAyGrgMUTJY9SA0RXDMYVxjqEYfFEGzITEyUxkgO9qhEuQKF1CI/i69BiCB5AwPcAQpEfClBUDcR7yF+cxEFmR0NXDVFz5YirqwWaz2Ww2W9R9AE/cBAw+cEeMAAAAAElFTkSuQmCC', 'base64')
|
||||
public static img1xWhite: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAABUUlEQVRIx+XWP0tCURjH8cf+IBLaELSEvoIcWkRHX0AQbm4tbQ7uTi4XhIbAxfdQQ9HqkK05RINIgZtUgoOQ6A3026Sc9D73jzblb73P/XAOnPM8R2RTwyFF7rmjwMGqxC5n3PLNLDY3nLITDDnhih5O+eCSpB9inyLPeKVJgZgbk+QTv3nnWIcaBMmDDo0DQWMdCpj/A3W4oLo+9MqRiAgv60EzJmaeNB2aGr82qPK1wOzxaFRMdag/L3pjW4QMA5WBvg61ja1siYiQoakw0NahulFWI2R8WWagrkPlX4VzypGBsg5lF0prhFQGsjoUXmpn15zz5Mj0CLt1JMv3/bDcO2QC2xdjk/BqttYfrEdEhAgdT6ZDxM8ASDF0ZYak/I6jHBOVmZALMtnyjByZEfmgkzZNd4npkl5laEepGIfBpkJ09UdEnBItWpSIb+xL6gcK3j+JspcAUAAAAABJRU5ErkJggg==', 'base64')
|
||||
public static img2xWhite: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAQAAAD/5HvMAAACZklEQVRo3u3YsWsTYRiA8VcNCa3UIoJLoVsXt0KHKEWwYAfN4l4c3Bq6SCGIf0GkdJDSsYOLYCFChVKXLi7B0dKAEAgVSlpqpVBKsJLmcajFXHKXu/e7+3IZ7llzvPmF9458iUhSUtLAx11esMwSz7kdNyXNMzb4w1W/+cATbsSDmeQtP3Grzhvu9XdFL/mGX1/JW19h14r8srnCHivyK+oVBlxRf1bIQ9WKgqwwa47J8B4bvSNtBiphq3UTTg6bPdWDPlsFbelB+1ZB+3pQyyqopQdZLgEloAQU+h2rlPg+KKAWr7kuwjVeDQKoxULbnC9xgxwcEYrxgjo4IqzHCerm3KcZH6ibM8lxdDe1xyejzAPu8JhKGA5NPejUddAPRv69fouyMQdO9aAD10HLbVf8J2k5cKAHVVwHLTmuuSTpOVDRgzZdB9W42UXSc2BTD1r1GPWRlOO6lAEHVvWgec9hpU6EmgPzetBUj3EepMAcmNKD0jR0JAWnYfRjmq2eQztICo7Jz0QRERZ8xraRVBw6n8ugoHEufAZ/uvzHh0cqzgXjYhbbvsN/sUHZF+5sW0xjDhvNmYMy1CPn1MmIeRQiBxUkTIxwFCnn6Or4Yk7KRwrKS9hIsRsZZ9f7W1BDynoeZ3U1Q/wl3EEqRgIqSlSRcZyfzSqHety7SGMchuIcMibRRpYzY85ZZHePgzTLuRHnnFmxE7mehzb3GuTEXkxzouKcMC12Y4KdwJwdJsR+DLMWiLPGsPQrZqn1xNSs3ciepCEKHgfXYwoMSRwxyiJVB6bKIqMSb8ywwh57rDAjSUlJA99fMZuE+02zWzkAAAAASUVORK5CYII=', 'base64')
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// 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) {
|
||||
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))
|
||||
|
||||
return decoded
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// 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/main/src/app/cose-js/sign.js
|
||||
|
||||
import base45 from 'base45';
|
||||
import pako from 'pako';
|
||||
import cbor from 'cbor-js';
|
||||
|
||||
export function typedArrayToBufferSliced(array: Uint8Array): ArrayBuffer {
|
||||
return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset);
|
||||
}
|
||||
|
||||
export function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||
var buffer = new ArrayBuffer(array.length);
|
||||
|
||||
array.map(function (value, i) {
|
||||
return buffer[i] = value;
|
||||
})
|
||||
|
||||
return array.buffer;
|
||||
}
|
||||
|
||||
export function decodeData(data: string): Object {
|
||||
|
||||
if (data.startsWith('HC1')) {
|
||||
data = data.substring(3);
|
||||
|
||||
if (data.startsWith(':')) {
|
||||
data = data.substring(1);
|
||||
} else {
|
||||
console.log("Warning: unsafe HC1: header");
|
||||
}
|
||||
} else {
|
||||
console.log("Warning: no HC1: header");
|
||||
}
|
||||
|
||||
var arrayBuffer: Uint8Array = base45.decode(data);
|
||||
|
||||
if (arrayBuffer[0] == 0x78) {
|
||||
arrayBuffer = pako.inflate(arrayBuffer);
|
||||
}
|
||||
|
||||
var payloadArray: Array<Uint8Array> = cbor.decode(typedArrayToBuffer(arrayBuffer));
|
||||
|
||||
if (!Array.isArray(payloadArray)) {
|
||||
throw new Error('Expecting Array');
|
||||
}
|
||||
|
||||
if (payloadArray.length !== 4) {
|
||||
throw new Error('Expecting Array of length 4');
|
||||
}
|
||||
|
||||
var plaintext: Uint8Array = payloadArray[2];
|
||||
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
|
||||
|
||||
return decoded;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
img1xblack: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAABU0lEQVR4AWIYaWAUiAExoB06gGggDOMwripJthAgGwBoCGYDBIDIgAFBwDAABoJjhBgEAEChBBBaAA0JyeKAqR0hmnWx3p5o8MHdvfd9Z7SHH8Dr723iCpdoYBOZtoJ9XOALYghxjj0sw1k7OEEAiekVxyjBShto4h6SUg8N5KGqhCHEshdsI3FdiCM3SNwnxA1uKxKXZm3QfJCPQ3RmYVAfW5j2YH+QfkweQ1uDviEmdNHBR8SYddxCDOC2ojeI4RlL+K2Kd8UYcFvRE8TQxyKmVdFLOAbcVnQNMeEUCzCKPQbcVnQEiRilGQNuK9qFRI1SjAG3Fa0iiDh8hgPcQWIKwG1dHsQyD+qKCCGWhCgiVZ7T7yhagw9JyQe37FTGCKI0QhlWq2GiGDNBDU6qYwyJaYw6nFbBABJhgAoyKYc2QoghRBs5ZF4BLTz+aaGAef+nHwt5/579e2c2AAAAAElFTkSuQmCC', 'base64'),
|
||||
img2xblack: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACZ0lEQVR4Ae3TA8xcURRF4dq2ozqqbRtxUjeq7Zi127C2bXvSsLZtu/PP6ardjs577765K/li7mQnizGbzWaz2Wx50BXjMRYdkR0JXRq0xVq8g/ziDZaiGVIiYSqLybgPCdMtjEZpP1+oP45CYnQYPZDd7xeKnPMX1L+QAoULmnUhX12wVrwupHjBKnC8tFgEMcRcpIFjrYQYZhkcqQXEUM2h3haIoTZDvRsQQ92AeiGIoUJQTwxnB7ID2YHsQHYgO5B7zmMlztiBfhbCCKQAJUuOYbADIYRe+FP7TB1IfxyiUaYOpD8O0TJzB9IfpyqCZg6kP05ZPIBESL0gJAIBVENONMRJF8cJQr1nkDBdRWb8WBYEHB8HeAb1bkPCNB5E/xlJfRzgNtQ7CQnTWNB/R9IfBzgJ9TZCwnQJGcMYSX8cYCPUmw6JwCqkwt9K5cg4wHSo1x0SoZX/GUJ/HKA71KsAURhJdxygAtRLg1cKI2mP8wpp4EibIQoj6YwDbIZj9YIojKQzDtALjlUESZAYrEN2fK2u4jhJKAJH2wmJ0UOsRQBJECU74XjtIYZoD8dLi1sQj7uFtHClIRCPGwLXyox7EI+6h8xwtR4Qj+oB10uFExCPOYFU8ERVEIR4RBBV4KlGQTxiFDxXWgQgLgsgLTxZQdyBuOQOCsLTVcELiMNeoAqMqBHeQhzyFo1gVC3wCqLsFVrAyGrgMUTJY9SA0RXDMYVxjqEYfFEGzITEyUxkgO9qhEuQKF1CI/i69BiCB5AwPcAQpEfClBUDcR7yF+cxEFmR0NXDVFz5YirqwWaz2Ww2W9R9AE/cBAw+cEeMAAAAAElFTkSuQmCC', 'base64'),
|
||||
img1xwhite: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAABUUlEQVRIx+XWP0tCURjH8cf+IBLaELSEvoIcWkRHX0AQbm4tbQ7uTi4XhIbAxfdQQ9HqkK05RINIgZtUgoOQ6A3026Sc9D73jzblb73P/XAOnPM8R2RTwyFF7rmjwMGqxC5n3PLNLDY3nLITDDnhih5O+eCSpB9inyLPeKVJgZgbk+QTv3nnWIcaBMmDDo0DQWMdCpj/A3W4oLo+9MqRiAgv60EzJmaeNB2aGr82qPK1wOzxaFRMdag/L3pjW4QMA5WBvg61ja1siYiQoakw0NahulFWI2R8WWagrkPlX4VzypGBsg5lF0prhFQGsjoUXmpn15zz5Mj0CLt1JMv3/bDcO2QC2xdjk/BqttYfrEdEhAgdT6ZDxM8ASDF0ZYak/I6jHBOVmZALMtnyjByZEfmgkzZNd4npkl5laEepGIfBpkJ09UdEnBItWpSIb+xL6gcK3j+JspcAUAAAAABJRU5ErkJggg==', 'base64'),
|
||||
img2xwhite: new Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAQAAAD/5HvMAAACZklEQVRo3u3YsWsTYRiA8VcNCa3UIoJLoVsXt0KHKEWwYAfN4l4c3Bq6SCGIf0GkdJDSsYOLYCFChVKXLi7B0dKAEAgVSlpqpVBKsJLmcajFXHKXu/e7+3IZ7llzvPmF9458iUhSUtLAx11esMwSz7kdNyXNMzb4w1W/+cATbsSDmeQtP3Grzhvu9XdFL/mGX1/JW19h14r8srnCHivyK+oVBlxRf1bIQ9WKgqwwa47J8B4bvSNtBiphq3UTTg6bPdWDPlsFbelB+1ZB+3pQyyqopQdZLgEloAQU+h2rlPg+KKAWr7kuwjVeDQKoxULbnC9xgxwcEYrxgjo4IqzHCerm3KcZH6ibM8lxdDe1xyejzAPu8JhKGA5NPejUddAPRv69fouyMQdO9aAD10HLbVf8J2k5cKAHVVwHLTmuuSTpOVDRgzZdB9W42UXSc2BTD1r1GPWRlOO6lAEHVvWgec9hpU6EmgPzetBUj3EepMAcmNKD0jR0JAWnYfRjmq2eQztICo7Jz0QRERZ8xraRVBw6n8ugoHEufAZ/uvzHh0cqzgXjYhbbvsN/sUHZF+5sW0xjDhvNmYMy1CPn1MmIeRQiBxUkTIxwFCnn6Or4Yk7KRwrKS9hIsRsZZ9f7W1BDynoeZ3U1Q/wl3EEqRgIqSlSRcZyfzSqHety7SGMchuIcMibRRpYzY85ZZHePgzTLuRHnnFmxE7mehzb3GuTEXkxzouKcMC12Y4KdwJwdJsR+DLMWiLPGsPQrZqn1xNSs3ciepCEKHgfXYwoMSRwxyiJVB6bKIqMSb8ywwh57rDAjSUlJA99fMZuE+02zWzkAAAAASUVORK5CYII=', 'base64'),
|
||||
}
|
185
src/pass.js
185
src/pass.js
|
@ -1,185 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const constants = require('./constants')
|
||||
const utils = require('./utils')
|
||||
|
||||
const { Payload } = require('./payload')
|
||||
const { toBuffer } = require('do-not-zip')
|
||||
const crypto = require('crypto')
|
||||
|
||||
exports.createPass = async function(data) {
|
||||
function getBufferHash(buffer) {
|
||||
// creating hash
|
||||
const sha = crypto.createHash('sha1');
|
||||
sha.update(buffer);
|
||||
return sha.digest('hex');
|
||||
}
|
||||
|
||||
async function signPassWithRemote(pass, payload) {
|
||||
// From pass-js
|
||||
// https://github.com/walletpass/pass-js/blob/2b6475749582ca3ea742a91466303cb0eb01a13a/src/pass.ts
|
||||
|
||||
// Creating new Zip file
|
||||
const zip = []
|
||||
|
||||
// Adding required files
|
||||
// Create pass.json
|
||||
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@2x.png', data: payload.img2x })
|
||||
zip.push({ path: 'logo.png', data: payload.img1x })
|
||||
zip.push({ path: 'logo@2x.png', data: payload.img2x })
|
||||
|
||||
// adding manifest
|
||||
// Construct manifest here
|
||||
const manifestJson = JSON.stringify(
|
||||
zip.reduce(
|
||||
(res, { path, data }) => {
|
||||
res[path] = getBufferHash(data);
|
||||
return res;
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
zip.push({ path: 'manifest.json', data: manifestJson });
|
||||
|
||||
// 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',
|
||||
headers: {
|
||||
'Accept': 'application/octet-stream',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
PassJsonHash: passHash,
|
||||
useBlackVersion: !payload.dark
|
||||
})
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const manifestSignature = await response.arrayBuffer()
|
||||
|
||||
zip.push({ path: 'signature', data: Buffer.from(manifestSignature) });
|
||||
|
||||
// finished!
|
||||
return toBuffer(zip);
|
||||
}
|
||||
|
||||
let valueSets
|
||||
|
||||
try {
|
||||
valueSets = await utils.getValueSets()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let payload
|
||||
|
||||
try {
|
||||
payload = new Payload(data, valueSets)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const qrCode = {
|
||||
message: payload.raw,
|
||||
format: "PKBarcodeFormatQR",
|
||||
messageEncoding: "utf-8"
|
||||
}
|
||||
|
||||
const pass = {
|
||||
passTypeIdentifier: constants.PASS_IDENTIFIER,
|
||||
teamIdentifier: constants.TEAM_IDENTIFIER,
|
||||
sharingProhibited: false,
|
||||
voided: false,
|
||||
formatVersion: 1,
|
||||
logoText: constants.NAME,
|
||||
organizationName: constants.NAME,
|
||||
description: constants.NAME,
|
||||
labelColor: payload.labelColor,
|
||||
foregroundColor: payload.foregroundColor,
|
||||
backgroundColor: payload.backgroundColor,
|
||||
serialNumber: payload.uvci,
|
||||
barcodes: [qrCode],
|
||||
barcode: qrCode,
|
||||
generic: {
|
||||
headerFields: [
|
||||
{
|
||||
key: "type",
|
||||
label: "Certificate Type",
|
||||
value: payload.certificateType
|
||||
}
|
||||
],
|
||||
primaryFields: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
value: payload.name
|
||||
}
|
||||
],
|
||||
secondaryFields: [
|
||||
{
|
||||
key: "dose",
|
||||
label: "Dose",
|
||||
value: payload.dose
|
||||
},
|
||||
{
|
||||
key: "dov",
|
||||
label: "Date of Vaccination",
|
||||
value: payload.dateOfVaccination,
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
}
|
||||
],
|
||||
auxiliaryFields: [
|
||||
{
|
||||
key: "vaccine",
|
||||
label: "Vaccine",
|
||||
value: payload.vaccineName
|
||||
},
|
||||
{
|
||||
key: "dob",
|
||||
label: "Date of Birth", value:
|
||||
payload.dateOfBirth,
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
}
|
||||
],
|
||||
backFields: [
|
||||
{
|
||||
key: "uvci",
|
||||
label: "Unique Certificate Identifier (UVCI)",
|
||||
value: payload.uvci
|
||||
},
|
||||
{
|
||||
key: "issuer",
|
||||
label: "Certificate Issuer",
|
||||
value: payload.certificateIssuer
|
||||
},
|
||||
{
|
||||
key: "country",
|
||||
label: "Country of Vaccination",
|
||||
value: payload.countryOfVaccination
|
||||
},
|
||||
{
|
||||
key: "manufacturer",
|
||||
label: "Manufacturer",
|
||||
value: payload.manufacturer
|
||||
},
|
||||
{
|
||||
key: "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."
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return await signPassWithRemote(pass, payload)
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
import {Constants} from "./constants";
|
||||
import {Payload, PayloadBody} from "./payload";
|
||||
import {ValueSets} from "./value_sets";
|
||||
import {toBuffer as createZip} from 'do-not-zip';
|
||||
import {v4 as uuid4} from 'uuid';
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
enum QrFormat {
|
||||
PKBarcodeFormatQR = 'PKBarcodeFormatQR',
|
||||
}
|
||||
|
||||
enum Encoding {
|
||||
utf8 = "utf-8",
|
||||
}
|
||||
|
||||
interface QrCode {
|
||||
message: string;
|
||||
format: QrFormat;
|
||||
messageEncoding: Encoding;
|
||||
}
|
||||
|
||||
interface Field {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
textAlignment?: string;
|
||||
}
|
||||
|
||||
interface PassStructureDictionary {
|
||||
headerFields: Array<Field>;
|
||||
primaryFields: Array<Field>;
|
||||
secondaryFields: Array<Field>;
|
||||
auxiliaryFields: Array<Field>;
|
||||
backFields: Array<Field>;
|
||||
}
|
||||
|
||||
interface SignData {
|
||||
PassJsonHash: string;
|
||||
useBlackVersion: boolean;
|
||||
}
|
||||
|
||||
export class PassData {
|
||||
passTypeIdentifier: string = Constants.PASS_IDENTIFIER;
|
||||
teamIdentifier: string = Constants.TEAM_IDENTIFIER;
|
||||
sharingProhibited: boolean = false;
|
||||
voided: boolean = false;
|
||||
formatVersion: number = 1;
|
||||
logoText: string = Constants.NAME;
|
||||
organizationName: string = Constants.NAME;
|
||||
description: string = Constants.NAME;
|
||||
labelColor: string;
|
||||
foregroundColor: string;
|
||||
backgroundColor: string;
|
||||
serialNumber: string;
|
||||
barcodes: Array<QrCode>;
|
||||
barcode: QrCode;
|
||||
generic: PassStructureDictionary;
|
||||
|
||||
// Generates a sha1 hash from a given buffer
|
||||
private static getBufferHash(buffer: Buffer | string): string {
|
||||
const sha = crypto.createHash('sha1');
|
||||
sha.update(buffer);
|
||||
return sha.digest('hex');
|
||||
}
|
||||
|
||||
private static async signWithRemote(signData: SignData): Promise<ArrayBuffer> {
|
||||
// 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',
|
||||
headers: {
|
||||
'Accept': 'application/octet-stream',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signData)
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw Error("Error while singing Pass on server")
|
||||
}
|
||||
|
||||
return await response.arrayBuffer()
|
||||
}
|
||||
|
||||
static async generatePass(payloadBody: PayloadBody): Promise<Buffer> {
|
||||
// Get the Value Sets from GitHub
|
||||
const valueSets: ValueSets = await ValueSets.loadValueSets();
|
||||
|
||||
// Create Payload
|
||||
const payload: Payload = new Payload(payloadBody, valueSets);
|
||||
|
||||
// Create QR Code Object
|
||||
const qrCode: QrCode = {
|
||||
message: payload.rawData,
|
||||
format: QrFormat.PKBarcodeFormatQR,
|
||||
messageEncoding: Encoding.utf8,
|
||||
}
|
||||
|
||||
// Create pass data
|
||||
const pass: PassData = new PassData(payload, qrCode);
|
||||
|
||||
// Create new zip
|
||||
const zip = [] as { path: string; data: Buffer | string }[];
|
||||
|
||||
// Adding required fields
|
||||
|
||||
// Create pass.json
|
||||
const passJson = JSON.stringify(pass);
|
||||
|
||||
// Add pass.json to zip
|
||||
zip.push({path: 'pass.json', data: Buffer.from(passJson)});
|
||||
|
||||
// Add Images to zip
|
||||
zip.push({path: 'icon.png', data: payload.img1x})
|
||||
zip.push({path: 'icon@2x.png', data: payload.img2x})
|
||||
zip.push({path: 'logo.png', data: payload.img1x})
|
||||
zip.push({path: 'logo@2x.png', data: payload.img2x})
|
||||
|
||||
// Adding manifest
|
||||
// Construct manifest
|
||||
const manifestJson = JSON.stringify(
|
||||
zip.reduce(
|
||||
(res, {path, data}) => {
|
||||
res[path] = PassData.getBufferHash(data);
|
||||
return res;
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
|
||||
// Add Manifest JSON to zip
|
||||
zip.push({path: 'manifest.json', data: Buffer.from(manifestJson)});
|
||||
|
||||
// Create pass hash
|
||||
const passHash = PassData.getBufferHash(Buffer.from(passJson));
|
||||
|
||||
// Sign hash with server
|
||||
const manifestSignature = await PassData.signWithRemote({
|
||||
PassJsonHash: passHash,
|
||||
useBlackVersion: !payload.dark,
|
||||
});
|
||||
|
||||
// Add signature to zip
|
||||
zip.push({path: 'signature', data: Buffer.from(manifestSignature)});
|
||||
|
||||
return createZip(zip);
|
||||
}
|
||||
|
||||
private constructor(payload: Payload, qrCode: QrCode) {
|
||||
this.labelColor = payload.labelColor;
|
||||
this.foregroundColor = payload.foregroundColor;
|
||||
this.backgroundColor = payload.backgroundColor;
|
||||
this.serialNumber = uuid4(); // Generate random UUID v4
|
||||
this.barcodes = [qrCode];
|
||||
this.barcode = qrCode;
|
||||
this.generic = {
|
||||
headerFields: [
|
||||
{
|
||||
key: "type",
|
||||
label: "Certificate Type",
|
||||
value: payload.certificateType
|
||||
}
|
||||
],
|
||||
primaryFields: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
value: payload.name
|
||||
}
|
||||
],
|
||||
secondaryFields: [
|
||||
{
|
||||
key: "dose",
|
||||
label: "Dose",
|
||||
value: payload.dose
|
||||
},
|
||||
{
|
||||
key: "dov",
|
||||
label: "Date of Vaccination",
|
||||
value: payload.dateOfVaccination,
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
}
|
||||
],
|
||||
auxiliaryFields: [
|
||||
{
|
||||
key: "vaccine",
|
||||
label: "Vaccine",
|
||||
value: payload.vaccineName
|
||||
},
|
||||
{
|
||||
key: "dob",
|
||||
label: "Date of Birth",
|
||||
value: payload.dateOfBirth,
|
||||
textAlignment: "PKTextAlignmentRight"
|
||||
}
|
||||
],
|
||||
backFields: [
|
||||
{
|
||||
key: "uvci",
|
||||
label: "Unique Certificate Identifier (UVCI)",
|
||||
value: payload.uvci
|
||||
},
|
||||
{
|
||||
key: "issuer",
|
||||
label: "Certificate Issuer",
|
||||
value: payload.certificateIssuer
|
||||
},
|
||||
{
|
||||
key: "country",
|
||||
label: "Country of Vaccination",
|
||||
value: payload.countryOfVaccination
|
||||
},
|
||||
{
|
||||
key: "manufacturer",
|
||||
label: "Manufacturer",
|
||||
value: payload.manufacturer
|
||||
},
|
||||
{
|
||||
key: "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."
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
100
src/payload.js
100
src/payload.js
|
@ -1,100 +0,0 @@
|
|||
const img = require('./img')
|
||||
const consts = require('./constants')
|
||||
|
||||
exports.Payload = class {
|
||||
constructor(body, valueSets) {
|
||||
|
||||
const color = body["color"]
|
||||
const rawData = body["raw"]
|
||||
const decoded = body["decoded"]
|
||||
|
||||
if (!(color in consts.COLORS)) {
|
||||
throw new Error('Invalid color')
|
||||
}
|
||||
|
||||
const dark = (color !== 'white')
|
||||
|
||||
let backgroundColor = dark ? consts.COLORS[color] : consts.COLORS.white
|
||||
let labelColor = dark ? consts.COLORS.white : consts.COLORS.grey
|
||||
let foregroundColor = dark ? consts.COLORS.white : consts.COLORS.black
|
||||
let img1x = dark ? img.img1xwhite : img.img1xblack
|
||||
let img2x = dark ? img.img2xwhite : img.img2xblack
|
||||
|
||||
if (typeof rawData === 'undefined') {
|
||||
throw new Error('No raw payload')
|
||||
}
|
||||
|
||||
if (typeof decoded === 'undefined') {
|
||||
throw new Error('No decoded payload')
|
||||
}
|
||||
|
||||
const v = decoded["-260"]["1"]["v"][0]
|
||||
if (typeof v === 'undefined') {
|
||||
throw new Error('Failed to read vaccination information')
|
||||
}
|
||||
|
||||
const nam = decoded["-260"]["1"]["nam"]
|
||||
if (typeof nam === 'undefined') {
|
||||
throw new Error('Failed to read name')
|
||||
}
|
||||
|
||||
const dob = decoded["-260"]["1"]["dob"]
|
||||
if (typeof dob === 'undefined') {
|
||||
throw new Error('Failed to read date of birth')
|
||||
}
|
||||
|
||||
const firstName = nam["gn"]
|
||||
const lastName = nam["fn"]
|
||||
const name = lastName + ', ' + firstName
|
||||
|
||||
const doseIndex = v["dn"]
|
||||
const totalDoses = v["sd"]
|
||||
const dose = doseIndex + '/' + totalDoses
|
||||
|
||||
const dateOfVaccination = v["dt"]
|
||||
const uvci = v["ci"]
|
||||
const certificateIssuer = v["is"]
|
||||
|
||||
const medicalProducts = valueSets.medicalProducts["valueSetValues"]
|
||||
const countryCodes = valueSets.countryCodes["valueSetValues"]
|
||||
const manufacturers = valueSets.manufacturers["valueSetValues"]
|
||||
|
||||
const medicalProductKey = v["mp"]
|
||||
if(!(medicalProductKey in medicalProducts)) {
|
||||
throw new Error('Invalid medical product key')
|
||||
}
|
||||
|
||||
const countryCode = v["co"]
|
||||
if(!(countryCode in countryCodes)) {
|
||||
throw new Error('Invalid country code')
|
||||
}
|
||||
|
||||
const manufacturerKey = v["ma"]
|
||||
if(!(manufacturerKey in manufacturers)) {
|
||||
throw new Error('Invalid manufacturer')
|
||||
}
|
||||
|
||||
this.certificateType = 'Vaccination'
|
||||
|
||||
this.backgroundColor = backgroundColor
|
||||
this.labelColor = labelColor
|
||||
this.foregroundColor = foregroundColor
|
||||
this.img1x = img1x
|
||||
this.img2x = img2x
|
||||
|
||||
this.raw = rawData
|
||||
this.dark = dark
|
||||
|
||||
this.name = name
|
||||
this.dose = dose
|
||||
this.dateOfVaccination = dateOfVaccination
|
||||
this.dateOfBirth = dob
|
||||
this.uvci = uvci
|
||||
this.certificateIssuer = certificateIssuer
|
||||
this.medicalProductKey = medicalProductKey
|
||||
|
||||
this.countryOfVaccination = countryCodes[countryCode].display
|
||||
this.vaccineName = medicalProducts[medicalProductKey].display
|
||||
this.manufacturer = manufacturers[manufacturerKey].display
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
import {ValueSets} from "./value_sets";
|
||||
import {Constants} from "./constants";
|
||||
|
||||
export interface PayloadBody {
|
||||
color: string;
|
||||
rawData: string;
|
||||
decodedData: Uint8Array;
|
||||
}
|
||||
|
||||
export class Payload {
|
||||
certificateType: string = 'Vaccination';
|
||||
|
||||
rawData: string;
|
||||
|
||||
backgroundColor: string;
|
||||
labelColor: string;
|
||||
foregroundColor: string;
|
||||
img1x: Buffer;
|
||||
img2x: Buffer;
|
||||
dark: boolean;
|
||||
|
||||
name: string;
|
||||
dose: string;
|
||||
dateOfVaccination: string;
|
||||
dateOfBirth: string;
|
||||
uvci: string;
|
||||
certificateIssuer: string;
|
||||
medicalProductKey: string;
|
||||
|
||||
countryOfVaccination: string;
|
||||
vaccineName: string;
|
||||
manufacturer: string;
|
||||
|
||||
constructor(body: PayloadBody, valueSets: ValueSets) {
|
||||
|
||||
let colors = Constants.COLORS;
|
||||
|
||||
if (!(body.color in colors)) {
|
||||
throw new Error('Invalid color');
|
||||
}
|
||||
|
||||
const dark = body.color != 'white'
|
||||
|
||||
|
||||
// Get Vaccine, Name and Date of Birth information
|
||||
const vaccinationInformation = body.decodedData['-260']['1']['v'][0];
|
||||
const nameInformation = body.decodedData['-260']['1']['nam'];
|
||||
const dateOfBirthInformation = body.decodedData['-260']['1']['dob'];
|
||||
|
||||
if (vaccinationInformation == undefined) {
|
||||
throw new Error('Failed to read vaccination information');
|
||||
}
|
||||
|
||||
if (nameInformation == undefined) {
|
||||
throw new Error('Failed to read name');
|
||||
}
|
||||
|
||||
if (dateOfBirthInformation == undefined) {
|
||||
throw new Error('Failed to read date of birth');
|
||||
}
|
||||
|
||||
// Get Medical, country and manufacturer information
|
||||
const medialProductKey = vaccinationInformation['mp'];
|
||||
const countryCode = vaccinationInformation['co'];
|
||||
const manufacturerKey = vaccinationInformation['ma'];
|
||||
|
||||
if (!(medialProductKey in valueSets.medicalProducts)) {
|
||||
throw new Error('Invalid medical product key');
|
||||
}
|
||||
if (!(countryCode in valueSets.countryCodes)) {
|
||||
throw new Error('Invalid country code')
|
||||
}
|
||||
if (!(manufacturerKey in valueSets.manufacturers)) {
|
||||
throw new Error('Invalid manufacturer')
|
||||
}
|
||||
|
||||
|
||||
// Set Values
|
||||
this.rawData = body.rawData;
|
||||
|
||||
this.backgroundColor = dark ? colors[body.color] : colors.white
|
||||
this.labelColor = dark ? colors.white : colors.grey
|
||||
this.foregroundColor = dark ? colors.white : colors.black
|
||||
this.img1x = dark ? Constants.img1xWhite : Constants.img1xBlack
|
||||
this.img2x = dark ? Constants.img2xWhite : Constants.img2xBlack
|
||||
this.dark = dark;
|
||||
|
||||
this.name = `${nameInformation['fn']}, ${nameInformation['gn']}`;
|
||||
this.dose = `${vaccinationInformation['dn']}/${vaccinationInformation['sd']}`;
|
||||
this.dateOfVaccination = vaccinationInformation['dt'];
|
||||
this.dateOfBirth = dateOfBirthInformation;
|
||||
this.uvci = vaccinationInformation['ci'];
|
||||
this.certificateIssuer = vaccinationInformation['is'];
|
||||
this.medicalProductKey = medialProductKey; // TODO is this needed?
|
||||
|
||||
this.countryOfVaccination = valueSets.countryCodes[countryCode].display;
|
||||
this.vaccineName = valueSets.medicalProducts[medialProductKey].display;
|
||||
this.manufacturer = valueSets.manufacturers[manufacturerKey].display;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import {PNG} from 'pngjs'
|
||||
import * as PdfJS from 'pdfjs-dist'
|
||||
|
||||
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
|
||||
|
||||
// Processes a pdf file and returns it as ImageData
|
||||
export async function processPdf(file) {
|
||||
// Array to store ImageData information
|
||||
const typedArray = new Uint8Array(file)
|
||||
|
||||
// PDF scale, increase to read smaller QR Codes
|
||||
const pdfScale = 2;
|
||||
|
||||
// Get the canvas and context to render PDF
|
||||
const canvas = document.getElementById('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
let loadingTask = PdfJS.getDocument(typedArray)
|
||||
|
||||
await loadingTask.promise.then(async function (pdfDocument) {
|
||||
// Load last PDF page
|
||||
const pageNumber = pdfDocument.numPages;
|
||||
|
||||
const pdfPage = await pdfDocument.getPage(pageNumber)
|
||||
const viewport = pdfPage.getViewport({scale: pdfScale})
|
||||
|
||||
// Set correct canvas width / height
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
|
||||
// render PDF
|
||||
const renderTask = pdfPage.render({
|
||||
canvasContext: ctx,
|
||||
viewport,
|
||||
})
|
||||
|
||||
return await renderTask.promise
|
||||
})
|
||||
|
||||
// Return PDF Image Data
|
||||
return ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
// Processes a PNG File and returns it as ImageData
|
||||
export async function processPng(file) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let png = new PNG({filterType: 4})
|
||||
|
||||
png.parse(file, (error, data) => {
|
||||
if (error) {
|
||||
reject()
|
||||
}
|
||||
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import {PayloadBody} from "./payload";
|
||||
import {PNG} from 'pngjs'
|
||||
import * as PdfJS from 'pdfjs-dist'
|
||||
import jsQR, {QRCode} from "jsqr";
|
||||
import {decodeData} from "./decode";
|
||||
import {Result} from "@zxing/library";
|
||||
|
||||
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
|
||||
|
||||
export async function getPayloadBodyFromFile(file: File, color: string): Promise<PayloadBody> {
|
||||
// Read file
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
let imageData: ImageData;
|
||||
|
||||
switch (file.type) {
|
||||
case 'application/pdf':
|
||||
console.log('pdf')
|
||||
imageData = await getImageDataFromPdf(fileBuffer)
|
||||
break
|
||||
case 'image/png':
|
||||
console.log('png')
|
||||
imageData = await getImageDataFromPng(fileBuffer)
|
||||
break
|
||||
default:
|
||||
throw Error('Invalid File Type')
|
||||
}
|
||||
|
||||
let code: QRCode;
|
||||
|
||||
try {
|
||||
code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: "dontInvert",
|
||||
});
|
||||
} catch (e) {
|
||||
throw Error("Could not decode QR Code from File");
|
||||
}
|
||||
|
||||
if (code == undefined) {
|
||||
throw Error("Could not find QR Code in provided File")
|
||||
}
|
||||
|
||||
// Get raw data
|
||||
let rawData = code.data;
|
||||
|
||||
// Decode Data
|
||||
let decodedData;
|
||||
|
||||
try {
|
||||
decodedData = decodeData(rawData);
|
||||
} catch (e) {
|
||||
throw Error("Invalid QR Code")
|
||||
}
|
||||
|
||||
return {
|
||||
rawData: rawData,
|
||||
decodedData: decodedData,
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPayloadBodyFromQR(qrCodeResult: Result, color: string): Promise<PayloadBody> {
|
||||
|
||||
// Get raw data
|
||||
let rawData = qrCodeResult.getText();
|
||||
|
||||
// Decode Data
|
||||
let decodedData;
|
||||
|
||||
try {
|
||||
decodedData = decodeData(rawData);
|
||||
} catch (e) {
|
||||
throw Error("Invalid QR Code")
|
||||
}
|
||||
|
||||
return {
|
||||
rawData: rawData,
|
||||
decodedData: decodedData,
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
async function getImageDataFromPng(fileBuffer: ArrayBuffer): Promise<ImageData> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let png = new PNG({filterType: 4})
|
||||
|
||||
png.parse(fileBuffer, (error, data) => {
|
||||
if (error) {
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData> {
|
||||
const typedArray = new Uint8Array(fileBuffer);
|
||||
const pdfScale = 2;
|
||||
|
||||
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
|
||||
const canvasContext = canvas.getContext('2d');
|
||||
|
||||
let loadingTask = PdfJS.getDocument(typedArray);
|
||||
|
||||
await loadingTask.promise.then(async function (pdfDocument) {
|
||||
// Load last PDF page
|
||||
const pageNumber = pdfDocument.numPages;
|
||||
|
||||
const pdfPage = await pdfDocument.getPage(pageNumber)
|
||||
const viewport = pdfPage.getViewport({scale: pdfScale})
|
||||
|
||||
// Set correct canvas width / height
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
|
||||
// render PDF
|
||||
const renderTask = pdfPage.render({
|
||||
canvasContext: canvasContext,
|
||||
viewport,
|
||||
})
|
||||
|
||||
return await renderTask.promise
|
||||
});
|
||||
|
||||
// Return PDF Image Data
|
||||
return canvasContext.getImageData(0, 0, canvas.width, canvas.height)
|
||||
}
|
16
src/utils.js
16
src/utils.js
|
@ -1,16 +0,0 @@
|
|||
const { VALUE_TYPES, VALUE_SET_BASE_URL } = require("./constants")
|
||||
const fetch = require("node-fetch")
|
||||
|
||||
exports.getValueSets = async function() {
|
||||
async function getJSONFromURL(url) {
|
||||
return await (await fetch(url)).json()
|
||||
}
|
||||
|
||||
let valueSets = {}
|
||||
|
||||
for (const [key, value] of Object.entries(VALUE_TYPES)) {
|
||||
valueSets[key] = await getJSONFromURL(VALUE_SET_BASE_URL + value)
|
||||
}
|
||||
|
||||
return valueSets
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
interface ValueTypes {
|
||||
medicalProducts: string;
|
||||
countryCodes: string;
|
||||
manufacturers: string;
|
||||
}
|
||||
|
||||
export class ValueSets {
|
||||
private static VALUE_SET_BASE_URL: string = 'https://raw.githubusercontent.com/ehn-dcc-development/ehn-dcc-valuesets/main/';
|
||||
private static VALUE_TYPES: ValueTypes = {
|
||||
medicalProducts: 'vaccine-medicinal-product.json',
|
||||
countryCodes: 'country-2-codes.json',
|
||||
manufacturers: 'vaccine-mah-manf.json',
|
||||
}
|
||||
|
||||
medicalProducts: object;
|
||||
countryCodes: object;
|
||||
manufacturers: object;
|
||||
|
||||
|
||||
private constructor(medicalProducts: object, countryCodes: object, manufacturers: object) {
|
||||
this.medicalProducts = medicalProducts;
|
||||
this.countryCodes = countryCodes;
|
||||
this.manufacturers = manufacturers;
|
||||
}
|
||||
|
||||
public static async loadValueSets(): Promise<ValueSets> {
|
||||
// Load all Value Sets from GitHub
|
||||
let medicalProducts = await (await fetch(ValueSets.VALUE_SET_BASE_URL + ValueSets.VALUE_TYPES.medicalProducts)).json();
|
||||
let countryCodes = await (await fetch(ValueSets.VALUE_SET_BASE_URL + ValueSets.VALUE_TYPES.countryCodes)).json();
|
||||
let manufacturers = await (await fetch(ValueSets.VALUE_SET_BASE_URL + ValueSets.VALUE_TYPES.manufacturers)).json();
|
||||
|
||||
return new ValueSets(medicalProducts['valueSetValues'], countryCodes['valueSetValues'], manufacturers['valueSetValues']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue