Merge pull request #28 from covidpass-org/typescript

Transition to typescript
This commit is contained in:
Marvin Sextro 2021-07-03 15:39:05 +02:00 committed by GitHub
commit 626e516b22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 4646 additions and 8968 deletions

3
.gitignore vendored
View File

@ -30,3 +30,6 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
# Idea files
.idea

View File

@ -11,7 +11,7 @@ FROM node:14-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build
RUN yarn build
# Production image, copy all the files and run next
FROM node:14-alpine AS runner
@ -23,7 +23,8 @@ RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/next-i18next.config.js ./next-i18next.config.js
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules

View File

@ -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>
)
}

26
components/Alert.tsx Normal file
View File

@ -0,0 +1,26 @@
import {useTranslation} from 'next-i18next';
interface AlertProps {
onClose: () => void;
errorMessage: string;
}
function Alert(props: AlertProps): JSX.Element {
const { t } = useTranslation(['index', 'errors']);
return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mt-5 rounded relative" role="alert">
<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>{t('index:errorClose')}</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;

View File

@ -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;

View File

@ -1,304 +0,0 @@
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";
export default Form
function Form() {
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);
const [globalControls, setGlobalControls] = useState(undefined);
const [qrCode, setQrCode] = useState(undefined);
const [file, setFile] = useState(undefined);
const inputFile = useRef(undefined)
useEffect(() => {
if (inputFile && inputFile.current) {
inputFile.current.addEventListener('input', () => {
let selectedFile = inputFile.current.files[0];
if (selectedFile !== undefined) {
setQrCode(undefined);
setFile(selectedFile);
}
});
}
}, [inputFile])
async function showFileDialog() {
inputFile.current.click();
}
async function hideCameraView() {
if (globalControls !== undefined) {
globalControls.stop();
}
setIsCameraOpen(false);
}
async function showCameraView() {
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');
setGlobalControls(await codeReader.decodeFromVideoDevice(undefined, previewElem, (result, error, controls) => {
if (result !== undefined) {
setQrCode(result);
setFile(undefined);
controls.stop();
// Reset
setGlobalControls(undefined);
setIsCameraOpen(false);
}
}));
setIsCameraOpen(true);
}
return (
<div>
<form className="space-y-5" id="form" onSubmit={(e) => addToWallet(e)}>
<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
doctor, pharmacy, vaccination centre or online. Note that taking a picture does not work on
most devices yet.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<button
type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{isCameraOpen ? "Stop Camera" : "Start Camera"}
</button>
<button
type="button"
onClick={showFileDialog}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
Open File (PDF, PNG)
</button>
</div>
<video id="cameraPreview" className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
<input type='file'
id='file'
accept="application/pdf,image/png"
ref={inputFile}
style={{display: 'none'}}
/>
{(qrCode || file) &&
<div className="flex items-center space-x-1">
<svg className="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
</svg>
<span className="w-full">
{
qrCode && 'Found QR Code!'
}
{
file && file.name
}
</span>
</div>}
</div>
}/>
<Card step={2} heading="Pick a Color" content={
<div className="space-y-5">
<p>
Pick a background color for your pass.
</p>
<div className="relative inline-block w-full">
<select name="color" id="color"
className="bg-gray-200 dark:bg-gray-900 focus:outline-none w-full h-10 pl-3 pr-6 text-base rounded-md appearance-none cursor-pointer">
<option value="white">white</option>
<option value="black">black</option>
<option value="grey">grey</option>
<option value="green">green</option>
<option value="indigo">indigo</option>
<option value="blue">blue</option>
<option value="purple">purple</option>
<option value="teal">teal</option>
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-5 h-5 fill-current" viewBox="0 0 20 20">
<path
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd" fillRule="evenodd"/>
</svg>
</div>
</div>
</div>
}/>
<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.
In order for you to make an informed decision, please read the <a href="/privacy">Privacy
Policy</a>.
</p>
<label htmlFor="privacy" className="flex flex-row space-x-4 items-center">
<input type="checkbox" id="privacy" value="privacy" required className="h-4 w-4"/>
<p>
I accept the <a href="/privacy" className="underline">Privacy Policy</a>
</p>
</label>
<div className="flex flex-row items-center justify-start">
<button id="download" type="download"
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"}}>
<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"/>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</div>
</div>
</div>
}/>
</form>
<Alert/>
<canvas id="canvas" style={{display: "none"}}/>
</div>
)
}

250
components/Form.tsx Normal file
View File

@ -0,0 +1,250 @@
import {saveAs} from 'file-saver';
import React, {FormEvent, useEffect, useRef, useState} from "react";
import {BrowserQRCodeReader} from "@zxing/browser";
import {Result} from "@zxing/library";
import {useTranslation} from 'next-i18next';
import Link from 'next/link';
import Card from "./Card";
import Alert from "./Alert";
import {PayloadBody} from "../src/payload";
import {getPayloadBodyFromFile, getPayloadBodyFromQR} from "../src/process";
import {PassData} from "../src/pass";
function Form(): JSX.Element {
const { t } = useTranslation(['index', 'errors', 'common']);
// Whether camera is open or not
const [isCameraOpen, setIsCameraOpen] = useState<boolean>(false);
// Global camera controls
const [globalControls, setGlobalControls] = useState(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);
// Check if there is a translation and replace message accordingly
const setErrorMessage = (message: string) => {
const translation = t('errors:'.concat(message));
_setErrorMessage(translation !== message ? translation : message);
};
// 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', () => {
let selectedFile = inputFile.current.files[0];
if (selectedFile !== undefined) {
setQrCode(undefined);
setFile(selectedFile);
}
});
}
}, [inputFile])
// Show file Dialog
async function showFileDialog() {
inputFile.current.click();
}
// Hide camera view
async function hideCameraView() {
if (globalControls !== undefined) {
globalControls.stop();
}
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: HTMLVideoElement = document.querySelector('#cameraPreview');
// Set Global controls
setGlobalControls(
// Start decoding from video device
await codeReader.decodeFromVideoDevice(undefined,
previewElem,
(result, error, controls) => {
if (result !== undefined) {
setQrCode(result);
setFile(undefined);
controls.stop();
// 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('noFileOrQrCode')
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.message);
setLoading(false);
}
}
return (
<div>
<form className="space-y-5" id="form" onSubmit={addToWallet}>
<Card step="1" heading={t('index:selectCertificate')} content={
<div className="space-y-5">
<p>{t('index:selectCertificateDescription')}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<button
type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
</button>
<button
type="button"
onClick={showFileDialog}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{t('index:openFile')}
</button>
</div>
<video id="cameraPreview"
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
<input type='file'
id='file'
accept="application/pdf,image/png"
ref={inputFile}
style={{display: 'none'}}
/>
{(qrCode || file) &&
<div className="flex items-center space-x-1">
<svg className="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
</svg>
<span className="w-full truncate">
{
qrCode && t('index:foundQrCode')
}
{
file && file.name
}
</span>
</div>
}
</div>
}/>
<Card step="2" heading={t('index:pickColor')} content={
<div className="space-y-5">
<p>{t('index:pickColorDescription')}</p>
<div className="relative inline-block w-full">
<select name="color" id="color"
className="bg-gray-200 dark:bg-gray-900 focus:outline-none w-full h-10 pl-3 pr-6 text-base rounded-md appearance-none cursor-pointer">
<option value="white">{t('index:colorWhite')}</option>
<option value="black">{t('index:colorBlack')}</option>
<option value="grey">{t('index:colorGrey')}</option>
<option value="green">{t('index:colorGreen')}</option>
<option value="indigo">{t('index:colorIndigo')}</option>
<option value="blue">{t('index:colorBlue')}</option>
<option value="purple">{t('index:colorPurple')}</option>
<option value="teal">{t('index:colorTeal')}</option>
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-5 h-5 fill-current" viewBox="0 0 20 20">
<path
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd" fillRule="evenodd"/>
</svg>
</div>
</div>
</div>
}/>
<Card step="3" heading={t('index:addToWallet')} content={
<div className="space-y-5">
<p>
{t('index:dataPrivacyDescription')}
<Link href="/privacy">
<a>
{t('index:privacyPolicy')}
</a>
</Link>.
</p>
<label htmlFor="privacy" className="flex flex-row space-x-4 items-center">
<input type="checkbox" id="privacy" value="privacy" required className="h-4 w-4"/>
<p>
{t('index:iAcceptThe')}&nbsp;
<Link href="/privacy">
<a className="underline">
{t('index:privacyPolicy')}
</a>
</Link>.
</p>
</label>
<div className="flex flex-row items-center justify-start">
<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">
{t('index:addToWallet')}
</button>
<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"/>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</div>
</div>
</div>
}/>
</form>
<canvas id="canvas" style={{display: "none"}}/>
{
errorMessage && <Alert errorMessage={errorMessage} onClose={() => setErrorMessage(undefined)}/>
}
</div>
)
}
export default Form;

View File

@ -1,8 +1,10 @@
import {useTranslation} from 'next-i18next';
import Link from 'next/link'
export default Logo
function Logo(): JSX.Element {
const { t } = useTranslation('common');
function Logo() {
return (
<Link href="/">
<a className="flex flex-row items-center p-3 justify-center space-x-1">
@ -18,9 +20,11 @@ function Logo() {
</g>
</svg>
<h1 className="text-3xl font-bold">
CovidPass
{t('common:title')}
</h1>
</a>
</Link>
)
}
}
export default Logo

View File

@ -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>
)
}

40
components/Page.tsx Normal file
View File

@ -0,0 +1,40 @@
import {useTranslation} from 'next-i18next';
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 {
const { t } = useTranslation('common');
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>{t('common:title')}</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 flex-wrap">
<a href="https://www.paypal.com/paypalme/msextro" className="hover:underline">{t('common:donate')}</a>
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline">{t('common:gitHub')}</a>
<Link href="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link>
</nav>
</footer>
</main>
</div>
</div>
)
}
export default Page

3
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

9
next-i18next.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
localeExtension: 'yml',
defaultLanguage: 'en',
fallbackLng: ['en'],
},
};

View File

@ -13,10 +13,6 @@ export default {
cardType: 'summary_large_image',
},
additionalLinkTags: [
{
rel: 'icon',
href: 'https://covidpass.marvinsextro.de/favicon.ico',
},
{
rel: 'apple-touch-icon',
href: 'https://covidpass.marvinsextro.de/apple-touch-icon.png',

5
next.config.js Normal file
View File

@ -0,0 +1,5 @@
const {i18n} = require('./next-i18next.config');
module.exports = {
i18n,
};

7823
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,25 +11,30 @@
"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",
"jpeg-js": "^0.4.3",
"jsqr": "^1.4.0",
"next": "latest",
"next-i18next": "^8.5.1",
"next-seo": "^4.26.0",
"node-fetch": "^2.6.1",
"pdfjs-dist": "^2.5.207",
"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"
}
}

View File

@ -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} />
</>
);
}
}

17
pages/_app.tsx Normal file
View File

@ -0,0 +1,17 @@
import 'tailwindcss/tailwind.css';
import {DefaultSeo} from 'next-seo';
import SEO from '../next-seo.config';
import type {AppProps} from 'next/app';
import {appWithTranslation} from 'next-i18next';
function MyApp({Component, pageProps}: AppProps): JSX.Element {
return (
<>
<DefaultSeo {...SEO} />
<Component {...pageProps} />
</>
)
}
export default appWithTranslation(MyApp);

View File

@ -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;

View File

@ -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 })
}

11
pages/api/config.tsx Normal file
View File

@ -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})
}

View File

@ -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>
}/>
}/>
)
}

52
pages/imprint.tsx Normal file
View File

@ -0,0 +1,52 @@
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import Page from '../components/Page'
import Card from '../components/Card'
function Imprint(): JSX.Element {
const { t } = useTranslation(['common', 'index', 'imprint']);
return (
<Page content={
<Card step="§" heading={t('common:imprint')} content={
<div className="space-y-2">
<p className="font-bold">{t('imprint:heading')}</p>
<p>
Marvin Sextro<br />
Wilhelm-Busch-Str. 8A<br />
30167 Hannover<br />
</p>
<p className="font-bold">{t('imprint:contact')}</p>
<p>
<a href="mailto:marvin.sextro@gmail.com" className="underline">marvin.sextro@gmail.com</a>
</p>
<p className="font-bold">{t('imprint:euDisputeResolution')}</p>
<p>{t('imprint:euDisputeResolutionParagraph')}</p>
<p className="font-bold">{t('imprint:consumerDisputeResolution')}</p>
<p>{t('imprint:consumerDisputeResolutionParagraph')}</p>
<p className="font-bold">{t('imprint:liabilityForContents')}</p>
<p>{t('imprint:liabilityForContentsParagraph')}</p>
<p className="font-bold">{t('imprint:liabilityForLinks')}</p>
<p>{t('imprint:liabilityForLinksParagraph')}</p>
<p className="font-bold">{t('imprint:credits')}</p>
<p>
{t('imprint:creditsSource')}
<br />
{t('imprint:creditsTranslation')}
</p>
</div>
}/>
}/>
)
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['index', 'imprint', 'common']))
}
}
}
export default Imprint;

View File

@ -1,52 +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.',
},
{
url: 'https://covidpass.marvinsextro.de/favicon.png',
width: 500,
height: 500,
alt: 'CovidPass',
},
],
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>
} />
</>
)
}

58
pages/index.tsx Normal file
View File

@ -0,0 +1,58 @@
import {NextSeo} from 'next-seo';
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import Form from '../components/Form';
import Card from '../components/Card';
import Page from '../components/Page';
function Index(): JSX.Element {
const { t } = useTranslation(['common', 'index', 'errors']);
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>{t('common:subtitle')}&nbsp;{t('index:iosHint')}</p>
}/>
<Form/>
</div>
}/>
</>
)
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'index', 'errors'])),
},
};
}
export default Index;

View File

@ -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>
}/>
}/>
)
}

177
pages/privacy.tsx Normal file
View File

@ -0,0 +1,177 @@
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import Page from '../components/Page'
import Card from '../components/Card'
function Privacy(): JSX.Element {
const { t } = useTranslation(['common', 'index', 'privacy']);
return (
<Page content={
<Card step="i" heading={t('common:privacyPolicy')} content={
<div className="space-y-2">
<p>{t('privacy:gdprNotice')}</p>
<p className="font-bold">{t('privacy:generalInfo')}</p>
<div className="px-4">
<ul className="list-disc">
<li>{t('privacy:generalInfoProcess')}</li>
<li>{t('privacy:generalInfoStoring')}</li>
<li>{t('privacy:generalInfoThirdParties')}</li>
<li>{t('privacy:generalInfoHttps')}</li>
<li>{t('privacy:generalInfoLocation')}</li>
<li>
{t('privacy:generalInfoGitHub')}
&nbsp;
<a href="https://github.com/marvinsxtr/covidpass" className="underline">
GitHub
</a>.
</li>
<li>
{t('privacy:generalInfoLockScreen')}
&nbsp;
<a href="https://support.apple.com/guide/iphone/control-access-information-lock-screen-iph9a2a69136/ios" className="underline">
{t('privacy:settings')}
</a>.
</li>
<li>
{t('privacy:generalInfoProvider')}
&nbsp;
<a href="https://www.hetzner.com/de/rechtliches/datenschutz/" className="underline">
{t('privacy:privacyPolicy')}
</a>
&nbsp;
{t('privacy:andThe')}
&nbsp;
<a href="https://docs.hetzner.com/general/general-terms-and-conditions/data-privacy-faq/privacy.tsx" className="underline">
{t('privacy:dataPrivacyFaq')}
</a>.
</li>
</ul>
</div>
<p className="font-bold">{t('privacy:contact')}</p>
<p>
Marvin Sextro<br/>
Wilhelm-Busch-Str. 8A<br/>
30167 Hannover<br/>
{t('privacy:email')}:
&nbsp;
<a href="mailto:marvin.sextro@gmail.com">marvin.sextro@gmail.com</a>
<br/>
{t('privacy:website')}:
&nbsp;
<a href="https://marvinsextro.de" className="underline">https://marvinsextro.de</a>
</p>
<p className="font-bold">{t('privacy:process')}</p>
<p>{t('privacy:processFirst')}:</p>
<div className="px-4">
<ul className="list-disc">
<li>{t('privacy:processRecognizing')}</li>
<li>{t('privacy:processDecoding')}</li>
<li>{t('privacy:processAssembling')}</li>
<li>{t('privacy:processGenerating')}</li>
<li>{t('privacy:processSending')}</li>
</ul>
</div>
<p>{t('privacy:processSecond')}:</p>
<div className="px-4">
<ul className="list-disc">
<li>{t('privacy:processReceiving')}</li>
<li>{t('privacy:processSigning')}</li>
<li>{t('privacy:processSendingBack')}</li>
</ul>
</div>
<p>{t('privacy:processThird')}:</p>
<div className="px-4">
<ul className="list-disc">
<li>{t('privacy:processCompleting')}</li>
<li>{t('privacy:processSaving')}</li>
</ul>
</div>
<p className="font-bold">{t('privacy:locallyProcessedData')}</p>
<p>
{t('privacy:the')}
&nbsp;
<a href="https://github.com/ehn-dcc-development/ehn-dcc-schema" className="underline">
{t('privacy:schema')}
</a>
&nbsp;
{t('privacy:specification')}
</p>
<p className="font-bold">{t('privacy:serverProvider')}</p>
<p>{t('privacy:serverProviderIs')}</p>
<p>
<a href="https://www.hetzner.com/" className="underline">
Hetzner Online GmbH
</a>
<br />
Industriestr. 25<br />
91710 Gunzenhausen<br />
</p>
<p>{t('privacy:logFiles')}:</p>
<div className="px-4">
<ul className="list-disc">
<li>{t('privacy:logFilesBrowser')}</li>
<li>{t('privacy:logFilesOs')}</li>
<li>{t('privacy:logFilesReferrer')}</li>
<li>{t('privacy:logFilesTime')}</li>
<li>{t('privacy:logFilesIpAddress')}</li>
</ul>
</div>
<p className="font-bold">{t('privacy:rights')}</p>
<p>{t('privacy:rightsGranted')}:</p>
<div className="px-4">
<ul className="list-disc">
<li>{t('privacy:rightsAccess')}</li>
<li>{t('privacy:rightsErasure')}</li>
<li>{t('privacy:rightsRectification')}</li>
<li>{t('privacy:rightsPortability')}</li>
</ul>
</div>
<p className="font-bold">{t('privacy:thirdParties')}</p>
<div className="px-4">
<ul className="list-disc">
<li>
GitHub:
&nbsp;
<a href="https://docs.github.com/en/github/site-policy/github-privacy-statement" className="underline">
{t('common:privacyPolicy')}
</a>
</li>
<li>
PayPal:
&nbsp;
<a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full?locale.x=en_EN" className="underline">
{t('common:privacyPolicy')}
</a>
</li>
<li>
Gmail/Google:
&nbsp;
<a href="https://policies.google.com/privacy?hl=en-US" className="underline">
{t('common:privacyPolicy')}
</a>
</li>
<li>
{t('privacy:appleSync')}:
&nbsp;
<a href="https://www.apple.com/legal/privacy/en-ww/privacy.tsx" className="underline">
{t('common:privacyPolicy')}
</a>
</li>
</ul>
</div>
</div>
}/>
}/>
)
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['index', 'privacy', 'common'])),
},
};
}
export default Privacy;

View File

@ -0,0 +1,6 @@
title: CovidPass
subtitle: Übertrage Deine digitalen EU COVID-Impfzertifikate in Deine Wallet-Apps.
privacyPolicy: Datenschutz
donate: Unterstützen
gitHub: GitHub
imprint: Impressum

View File

@ -0,0 +1,14 @@
noFileOrQrCode: Bitte scanne einen QR-Code oder wähle eine Datei aus
signatureFailed: Fehler beim Signieren der Karte auf dem Server
decodingFailed: Dekodierung der QR-Code-Daten fehlgeschlagen
invalidColor: Ungültige Farbe
vaccinationInfo: Impfinformationen konnten nicht gelesen werden
nameMissing: Name konnte nicht gelesen werden
dobMissing: Geburtsdatum konnte nicht gelesen werden
invalidMedicalProduct: Ungültiges Medizinprodukt
invalidCountryCode: Ungültiger Ländercode
invalidManufacturer: Ungültiger Hersteller
invalidFileType: Ungültiger Dateityp
couldNotDecode: Dekodierung aus QR-Code fehlgeschlagen
couldNotFindQrCode: QR-Code konnte in der ausgewählten Datei nicht gefunden werden
invalidQrCode: Ungültiger QR-Code

View File

@ -0,0 +1,31 @@
heading: Angaben gemäß § 5 TMG
contact: Kontakt
euDisputeResolution: EU-Streitschlichtung
euDisputeResolutionParagraph: |
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: https://ec.europa.eu/consumers/odr.
Unsere E-Mail-Adresse finden Sie oben im Impressum.
consumerDisputeResolution: Verbraucherstreitbeilegung / Universalschlichtungsstelle
consumerDisputeResolutionParagraph: Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
liabilityForContents: Haftung für Inhalte
liabilityForContentsParagraph: |
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den
allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht
verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu
forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen
Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der
Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden
Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
liabilityForLinks: Haftung für Links
liabilityForLinksParagraph: |
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben.
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der
verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten
Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte
waren zum Zeitpunkt der Verlinkung nicht erkennbar.
Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer
Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links
umgehend entfernen.
credits: Quellen
creditsSource: Mit Auszügen aus https://www.e-recht24.de/impressum-generator.html
creditsTranslation: Übersetzt mit https://www.DeepL.com/Translator (kostenlose Version)

View File

@ -0,0 +1,26 @@
iosHint: Bitte verwende unter iOS den Safari Browser.
errorClose: Schließen
selectCertificate: Zertifikat auswählen
selectCertificateDescription: |
Scanne den QR-Code auf Deinem Zertifikat oder wähle einen Screenshot oder eine PDF-Datei mit dem QR-Code.
Bitte beachte, dass die Auswahl einer Datei direkt von der Kamera nicht unterstützt wird.
stopCamera: Kamera stoppen
startCamera: Kamera starten
openFile: Datei auswählen (PDF, PNG)
foundQrCode: QR-Code gefunden!
pickColor: Farbe auswählen
pickColorDescription: Wähle eine Hintergrundfarbe für Deine Karte.
colorWhite: weiß
colorBlack: schwarz
colorGrey: grau
colorGreen: grün
colorIndigo: indigo
colorBlue: blau
colorPurple: lila
colorTeal: blaugrün
addToWallet: Zu Wallet hinzufügen
dataPrivacyDescription: |
Datenschutz hat einen besonders hohen Stellenwert, wenn es um die Verarbeitung von gesundheitsbezogenen Daten geht.
Damit Du eine fundierte Entscheidung triffst, lies bitte die
iAcceptThe: Ich akzeptiere die
privacyPolicy: Datenschutzerklärung

View File

@ -0,0 +1,56 @@
gdprNotice: |
Unsere Datenschutzerklärung orientiert sich an den Vorgaben des europäischen Gesetzgebers für die Umsetzung der Datenschutz-Grundverordnung (DSGVO).
generalInfo: Generelle Informationen
generalInfoProcess: |
Der gesamte Prozess der Erstellung der Karte findet lokal im Browser statt.
Für den Schritt des Signierens wird nur eine gehashte Repräsentation der Daten an den Server geschickt.
generalInfoStoring: Ihre Daten werden nicht über die aktive Browsersitzung hinaus gespeichert und es werden keine Cookies verwendet.
generalInfoThirdParties: Keine Daten werden an Dritte weitergeleitet.
generalInfoHttps: Wir übertragen Ihre Daten sicher über https.
generalInfoLocation: Unser Server befindet sich in Nürnberg.
generalInfoGitHub: Der Quellcode dieser Seite ist öffentlich verfügbar auf
generalInfoLockScreen: Standardmäßig sind Apple Wallet Karten vom Sperrbildschirm erreichbar. Um dies zu ändern, gehen Sie in die
settings: Einstellungen
generalInfoProvider: |
Der Server-Anbieter verarbeitet Daten, um diese Seite bereitzustellen.
Um besser zu verstehen, welche Maßnahmen der Anbieter ergreift, um ihre Daten zu schützen, lesen Sie bitte auch die
privacyPolicy: Datenschutzerklärung
andThe: und das
dataPrivacyFaq: Datenschutz FAQ
contact: Kontakt
email: E-Mail
website: Webseite
process: Vereinfachte Erklärung des Prozesses
processFirst: Die folgenden Schritte werden zunächst in Ihrem Browser durchgeführt
processSecond: Dann werden die folgenden Schritte auf unserem Server abgearbeitet
processThird: Zuletzt werden die folgenden Schritte in Ihrem Browser durchgeführt
processRecognizing: Erkennung und Extraktion der QR-Code Daten aus dem von Ihnen gewählten Zertifikat
processDecoding: Dekodierung Ihrer persönlichen und gesundheitsbezogenen Daten aus den QR-Code Daten
processAssembling: Erstellung einer unvollständigen Karten-Datei aus Ihren Daten
processGenerating: Generierung einer Datei, welche Hashes der Daten aus der Karten-Datei beinhaltet
processSending: Senden der Datei mit den Hashes an unseren Server
processReceiving: Empfangen und kontrollieren der lokal generierten Hashes
processSigning: Signieren der Datei, welche die Hashes beinhaltet
processSendingBack: Zurücksenden der Signatur
processCompleting: Erstellung der signierten Karten-Datei aus der unvollständigen lokal generierten Datei und der Signatur
processSaving: Speichern der Datei auf dem Endgerät
locallyProcessedData: Lokal verarbeitete Daten
the: Das
schema: Digital Covid Certificate Schema
specification: enthält eine detaillierte Spezifikation darüber, welche Daten in dem QR-Code enthalten sind und lokal verarbeitet werden können.
serverProvider: Server-Anbieter
serverProviderIs: Unser Server-Anbieter ist
logFiles: Die folgenden Daten können gesammelt und in den Server Log-Dateien gespeichert werden
logFilesBrowser: Browser Typ und Version
logFilesOs: Verwendetes Betriebssystem
logFilesReferrer: Die Website von der unsere Seite erreicht wird (sog. Referrer)
logFilesTime: Das Datum und die Uhrzeit des Zugriffs
logFilesIpAddress: Die pseudonymisierten IP-Adressen
rights: Ihre Rechte
rightsGranted: In Übereinstimmung mit der DSGVO haben Sie die folgenden Rechte
rightsAccess: Das Recht auf Zugriff auf Ihre Daten; Sie haben das Recht, zu erfahren welche Daten über Sie gesammelt wurden und wie diese verarbeitet wurden.
rightsErasure: Das Recht, vergessen zu werden; Löschung Ihrer persönlichen Daten.
rightsRectification: Recht der Richtigstellung; Sie haben das Recht, inkorrekte Daten zu korrigieren.
rightsPortability: Recht auf Portabilität; Sie haben das Recht, Ihre Daten von einem informationsverarbeitenden System in ein anderes zu übertragen.
thirdParties: Verlinkte Dritte
appleSync: Apple kann Ihre Apple Wallet Karten über iCloud synchronisieren

View File

@ -0,0 +1,6 @@
title: CovidPass
subtitle: Add your EU Digital Covid Vaccination Certificates to your favorite wallet apps.
privacyPolicy: Privacy Policy
donate: Sponsor
gitHub: GitHub
imprint: Imprint

View File

@ -0,0 +1,14 @@
noFileOrQrCode: Please scan a QR Code, or select a file
signatureFailed: Error while signing pass on server
decodingFailed: Failed to decode QR code payload
invalidColor: Invalid color
vaccinationInfo: Failed to read vaccination information
nameMissing: Failed to read name
dobMissing: Failed to read date of birth
invalidMedicalProduct: Invalid medical product
invalidCountryCode: Invalid country code
invalidManufacturer: Invalid manufacturer
invalidFileType: Invalid file type
couldNotDecode: Could not decode QR code from file
couldNotFindQrCode: Could not find QR Code in provided file
invalidQrCode: Invalid QR code

View File

@ -0,0 +1,27 @@
heading: Information according to § 5 TMG
contact: Contact
euDisputeResolution: EU Dispute Resolution
euDisputeResolutionParagraph: |
The European Commission provides a platform for online dispute resolution (OS) https://ec.europa.eu/consumers/odr.
You can find our e-mail address in the imprint above.
consumerDisputeResolution: Consumer dispute resolution / universal arbitration board
consumerDisputeResolutionParagraph: We are not willing or obliged to participate in dispute resolution proceedings before a consumer arbitration board.
liabilityForContents: Liability for contents
liabilityForContentsParagraph: |
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.
liabilityForLinks: Liability for links
liabilityForLinksParagraph: |
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.
credits: Credits
creditsSource: With excerpts from https://www.e-recht24.de/impressum-generator.html
creditsTranslation: Translated with https://www.DeepL.com/Translator (free version)

View File

@ -0,0 +1,26 @@
iosHint: On iOS, please use the Safari Browser.
errorClose: Close
selectCertificate: Select Certificate
selectCertificateDescription: |
Please scan the QR code on your certificate or select a screenshot or PDF page with the QR code.
Note that selecting a file directly from camera is not supported.
stopCamera: Stop Camera
startCamera: Start Camera
openFile: Select File (PDF, PNG)
foundQrCode: Found QR Code!
pickColor: Pick a Color
pickColorDescription: Pick a background color for your pass.
colorWhite: white
colorBlack: black
colorGrey: grey
colorGreen: green
colorIndigo: indigo
colorBlue: blue
colorPurple: purple
colorTeal: teal
addToWallet: Add to Wallet
dataPrivacyDescription: |
Data privacy is of special importance when processing health-related data.
In order for you to make an informed decision, please read the
iAcceptThe: I accept the
privacyPolicy: Privacy Policy

View File

@ -0,0 +1,57 @@
gdprNotice: |
Our privacy policy is based on the terms used by the European legislator
for the adoption of the General Data Protection Regulation (GDPR).
generalInfo: General information
generalInfoProcess: |
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.
generalInfoStoring: Your data is not stored beyond the active browser session and the site does not use cookies.
generalInfoThirdParties: No data is sent to third parties.
generalInfoHttps: We transmit your data securely over https.
generalInfoLocation: Our server is hosted in Nuremberg, Germany.
generalInfoGitHub: The source code of this site is available on
generalInfoLockScreen: By default, Apple Wallet passes are accessible from the lock screen. This can be changed in the
settings: settings
generalInfoProvider: |
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
privacyPolicy: privacy policy
andThe: and the
dataPrivacyFaq: data privacy FAQ
contact: Contact
email: Email
website: Website
process: Simplified of the process
processFirst: First, the following steps happen locally in your browser
processSecond: Second, the following steps happen on our server
processThird: Finally, the following steps happen locally in your browser
processRecognizing: Recognizing and extracting the QR code data from your selected certificate
processDecoding: Decoding your personal and health-related data from the QR code payload
processAssembling: Assembling an incomplete pass file out of your data
processGenerating: Generating a file containing hashes of the data stored in the pass file
processSending: Sending only the file containing the hashes to our server
processReceiving: Receiving and checking the hashes which were generated locally
processSigning: Signing the file containing the hashes
processSendingBack: Sending the signature back
processCompleting: Assembling the signed pass file out of the incomplete file generated locally and the signature
processSaving: Saving the file on your device
locallyProcessedData: Locally processed data
the: The
schema: Digital Covid Certificate Schema
specification: contains a detailed specification of which data can be contained in the QR code and will be processed in your browser.
serverProvider: Server provider
serverProviderIs: Our server provider is
logFiles: The following data may be collected and stored in the server log files
logFilesBrowser: The browser types and versions used
logFilesOs: The operating system used by the accessing system
logFilesReferrer: The website from which an accessing system reaches our website (so-called referrers)
logFilesTime: The date and time of access
logFilesIpAddress: The pseudonymised IP addresses
rights: Your rights
rightsGranted: In accordance with the GDPR you have the following rights
rightsAccess: Right of access to your data; You have the right to know what data has been collected about you and how it was processed.
rightsErasure: Right to be forgotten; Erasure of your personal data.
rightsRectification: Right of rectification; You have the right to correct inaccurate data.
rightsPortability: Right of data portability; You have the right to transfer your data from one processing system into another.
thirdParties: Third parties linked
appleSync: Apple may sync your passes via iCloud

View File

@ -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

21
src/constants.ts Normal file
View File

@ -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')
}

View File

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

48
src/decode.ts Normal file
View File

@ -0,0 +1,48 @@
// 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);
}
}
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) || payloadArray.length !== 4) {
throw new Error('decodingFailed');
}
var plaintext: Uint8Array = payloadArray[2];
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
return decoded;
}

View File

@ -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'),
}

View File

@ -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)
}

230
src/pass.ts Normal file
View File

@ -0,0 +1,230 @@
import {toBuffer as createZip} from 'do-not-zip';
import {v4 as uuid4} from 'uuid';
import {Constants} from "./constants";
import {Payload, PayloadBody} from "./payload";
import {ValueSets} from "./value_sets";
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('signatureFailed')
}
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."
}
]
};
}
}

View File

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

100
src/payload.ts Normal file
View File

@ -0,0 +1,100 @@
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('invalidColor');
}
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('vaccinationInfo');
}
if (nameInformation == undefined) {
throw new Error('nameMissing');
}
if (dateOfBirthInformation == undefined) {
throw new Error('dobMissing');
}
// 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('invalidMedicalProduct');
}
if (!(countryCode in valueSets.countryCodes)) {
throw new Error('invalidCountryCode')
}
if (!(manufacturerKey in valueSets.manufacturers)) {
throw new Error('invalidManufacturer')
}
// 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.countryOfVaccination = valueSets.countryCodes[countryCode].display;
this.vaccineName = valueSets.medicalProducts[medialProductKey].display;
this.manufacturer = valueSets.manufacturers[manufacturerKey].display;
}
}

View File

@ -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)
})
})
}

128
src/process.ts Normal file
View File

@ -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('invalidFileType')
}
let code: QRCode;
try {
code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
} catch (e) {
throw Error('couldNotDecode');
}
if (code == undefined) {
throw Error('couldNotFindQrCode')
}
// Get raw data
let rawData = code.data;
// Decode Data
let decodedData;
try {
decodedData = decodeData(rawData);
} catch (e) {
throw Error('invalidQrCode')
}
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("invalidQrCode")
}
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)
}

View File

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

34
src/value_sets.ts Normal file
View File

@ -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']);
}
}

29
tsconfig.json Normal file
View File

@ -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"
]
}

3108
yarn.lock Normal file

File diff suppressed because it is too large Load Diff