Prepare for localization

This commit is contained in:
Marvin Sextro 2021-07-02 20:55:26 +02:00
parent 96dd9a1358
commit e9b2ccb6a8
23 changed files with 900 additions and 712 deletions

View File

@ -28,6 +28,7 @@ COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/next-i18next.config.js ./next-i18next.config.js
USER nextjs

View File

@ -1,18 +1,20 @@
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">
<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>
<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>

View File

@ -1,14 +1,17 @@
import Card from "./Card";
import {saveAs} from 'file-saver'
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 Card from "./Card";
import Alert from "./Alert";
import {PayloadBody} from "../src/payload";
import {getPayloadBodyFromFile, getPayloadBodyFromQR} from "../src/process";
import {PassData} from "../src/pass";
import Alert from "./Alert";
function Form(): JSX.Element {
const { t } = useTranslation(['index', 'errors', 'common']);
// Whether camera is open or not
const [isCameraOpen, setIsCameraOpen] = useState<boolean>(false);
@ -20,9 +23,15 @@ function Form(): JSX.Element {
const [qrCode, setQrCode] = useState<Result>(undefined);
const [file, setFile] = useState<File>(undefined);
const [errorMessage, setErrorMessage] = useState<string>(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)
@ -92,7 +101,7 @@ function Form(): JSX.Element {
setLoading(true);
if (!file && !qrCode) {
setErrorMessage("Please scan a QR Code, or select a file to scan")
setErrorMessage('noFileOrQrCode')
setLoading(false);
return;
}
@ -113,7 +122,7 @@ function Form(): JSX.Element {
saveAs(passBlob, 'covid.pkpass');
setLoading(false);
} catch (e) {
setErrorMessage(e.toString());
setErrorMessage(e.message);
setLoading(false);
}
}
@ -121,25 +130,21 @@ function Form(): JSX.Element {
return (
<div>
<form className="space-y-5" id="form" onSubmit={addToWallet}>
<Card step="1" heading="Select Certificate" content={
<Card step="1" heading={t('index:selectCertificate')} 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>
<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 ? "Stop Camera" : "Start Camera"}
{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">
Open File (PDF, PNG)
{t('index:openFile')}
</button>
</div>
@ -160,7 +165,7 @@ function Form(): JSX.Element {
</svg>
<span className="w-full truncate">
{
qrCode && 'Found QR Code!'
qrCode && t('index:foundQrCode')
}
{
file && file.name
@ -170,22 +175,20 @@ function Form(): JSX.Element {
}
</div>
}/>
<Card step="2" heading="Pick a Color" content={
<Card step="2" heading={t('index:pickColor')} content={
<div className="space-y-5">
<p>
Pick a background color for your pass.
</p>
<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">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>
<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">
@ -197,23 +200,27 @@ function Form(): JSX.Element {
</div>
</div>
}/>
<Card step="3" heading="Add to Wallet" content={
<Card step="3" heading={t('index:addToWallet')} 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>.
{t('index:dataPrivacyDescription')}&nbsp;
<a href="/privacy">
{t('index:privacyPolicy')}
</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>
{t('index:iAcceptThe')}&nbsp;
<a href="/privacy" className="underline">
{t('index:privacyPolicy')}
</a>
</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">
Add to Wallet
{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">

View File

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

View File

@ -1,3 +1,5 @@
import {useTranslation} from 'next-i18next';
import Head from 'next/head'
import Logo from './Logo'
import Link from 'next/link'
@ -7,10 +9,12 @@ interface PageProps {
}
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>CovidPass</title>
<title>{t('common:title')}</title>
<link rel="icon" href="/favicon.ico"/>
</Head>
<div>
@ -21,10 +25,10 @@ function Page(props: PageProps): JSX.Element {
<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>
<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>

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

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

5
next.config.js Normal file
View File

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

View File

@ -18,6 +18,7 @@
"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",

View File

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

View File

@ -1,57 +1,39 @@
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="Imprint" content={
<Card step="§" heading={t('common:imprint')} content={
<div className="space-y-2">
<p className="font-bold">Information according to § 5 TMG</p>
<p className="font-bold">{t('imprint:heading')}</p>
<p>
Marvin Sextro<br/>
Wilhelm-Busch-Str. 8A<br/>
30167 Hannover<br/>
Marvin Sextro<br />
Wilhelm-Busch-Str. 8A<br />
30167 Hannover<br />
</p>
<p className="font-bold">Contact</p>
<p className="font-bold">{t('imprint:contact')}</p>
<p>
marvin.sextro@gmail.com
<a href="mailto:marvin.sextro@gmail.com" className="underline">marvin.sextro@gmail.com</a>
</p>
<p className="font-bold">EU Dispute Resolution</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>
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)
{t('imprint:creditsSource')}
<br />
{t('imprint:creditsTranslation')}
</p>
</div>
}/>
@ -59,4 +41,12 @@ function Imprint(): JSX.Element {
)
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['index', 'imprint', 'common']))
}
}
}
export default Imprint;

View File

@ -1,10 +1,14 @@
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'
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
@ -33,10 +37,7 @@ function Index(): JSX.Element {
<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>
<p>{t('common:subtitle')}&nbsp;{t('index:iosHint')}</p>
}/>
<Form/>
@ -46,4 +47,12 @@ function Index(): JSX.Element {
)
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'index', 'errors'])),
},
};
}
export default Index;

View File

@ -1,222 +1,162 @@
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="Privacy Policy" content={
<Card step="i" heading={t('common:privacyPolicy')} 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>
<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>
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.
{t('privacy:generalInfoGitHub')}
&nbsp;
<a href="https://github.com/marvinsxtr/covidpass" className="underline">
GitHub
</a>.
</li>
<li>
Your data is not stored beyond the active browser session and the site does not use
cookies.
{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>
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>.
{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">Contact</p>
<p className="font-bold">{t('privacy: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:
{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>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>
<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>
Second, the following steps happen on our server:
</p>
<p>{t('privacy:processSecond')}:</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>
<li>{t('privacy:processReceiving')}</li>
<li>{t('privacy:processSigning')}</li>
<li>{t('privacy:processSendingBack')}</li>
</ul>
</div>
<p>
Finally, the following steps happen locally in your browser:
</p>
<p>{t('privacy:processThird')}:</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>
<li>{t('privacy:processCompleting')}</li>
<li>{t('privacy:processSaving')}</li>
</ul>
</div>
<p className="font-bold">Locally processed data</p>
<p className="font-bold">{t('privacy:locallyProcessedData')}</p>
<p>
The following data is processed on in your browser to generate the pass file.
{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>
Processed personal data contained in the QR code:
<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>Your first and last name</li>
<li>Your date of birth</li>
<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>
For each vaccination certificate contained in the QR code, the following data is processed:
</p>
<p className="font-bold">{t('privacy:rights')}</p>
<p>{t('privacy:rightsGranted')}:</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>
<li>{t('privacy:rightsAccess')}</li>
<li>{t('privacy:rightsErasure')}</li>
<li>{t('privacy:rightsRectification')}</li>
<li>{t('privacy:rightsPortability')}</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:
<p className="font-bold">{t('privacy:thirdParties')}</p>
<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.
GitHub:
&nbsp;
<a href="https://docs.github.com/en/github/site-policy/github-privacy-statement" className="underline">
{t('common:privacyPolicy')}
</a>
</li>
<li>
Right to be forgotten: Erasure of your personal data.
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>
Right of rectification: You have the right to correct inaccurate data.
Gmail/Google:
&nbsp;
<a href="https://policies.google.com/privacy?hl=en-US" className="underline">
{t('common:privacyPolicy')}
</a>
</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>
{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>
@ -226,4 +166,12 @@ function Privacy(): JSX.Element {
)
}
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: Add your EU Digital Covid Vaccination Certificates to your favorite wallet apps.
privacyPolicy: Privacy Policy
donate: Donate
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

@ -26,11 +26,7 @@ export function decodeData(data: string): Object {
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);
@ -41,12 +37,8 @@ export function decodeData(data: string): Object {
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');
if (!Array.isArray(payloadArray) || payloadArray.length !== 4) {
throw new Error('decodingFailed');
}
var plaintext: Uint8Array = payloadArray[2];

View File

@ -1,8 +1,9 @@
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";
import {toBuffer as createZip} from 'do-not-zip';
import {v4 as uuid4} from 'uuid';
const crypto = require('crypto')
@ -79,7 +80,7 @@ export class PassData {
})
if (response.status !== 200) {
throw Error("Error while singing Pass on server")
throw Error('signatureFailed')
}
return await response.arrayBuffer()

View File

@ -36,7 +36,7 @@ export class Payload {
let colors = Constants.COLORS;
if (!(body.color in colors)) {
throw new Error('Invalid color');
throw new Error('invalidColor');
}
const dark = body.color != 'white'
@ -48,15 +48,15 @@ export class Payload {
const dateOfBirthInformation = body.decodedData['-260']['1']['dob'];
if (vaccinationInformation == undefined) {
throw new Error('Failed to read vaccination information');
throw new Error('vaccinationInfo');
}
if (nameInformation == undefined) {
throw new Error('Failed to read name');
throw new Error('nameMissing');
}
if (dateOfBirthInformation == undefined) {
throw new Error('Failed to read date of birth');
throw new Error('dobMissing');
}
// Get Medical, country and manufacturer information
@ -65,13 +65,13 @@ export class Payload {
const manufacturerKey = vaccinationInformation['ma'];
if (!(medialProductKey in valueSets.medicalProducts)) {
throw new Error('Invalid medical product key');
throw new Error('invalidMedicalProduct');
}
if (!(countryCode in valueSets.countryCodes)) {
throw new Error('Invalid country code')
throw new Error('invalidCountryCode')
}
if (!(manufacturerKey in valueSets.manufacturers)) {
throw new Error('Invalid manufacturer')
throw new Error('invalidManufacturer')
}
@ -91,7 +91,6 @@ export class Payload {
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;

View File

@ -23,7 +23,7 @@ export async function getPayloadBodyFromFile(file: File, color: string): Promise
imageData = await getImageDataFromPng(fileBuffer)
break
default:
throw Error('Invalid File Type')
throw Error('invalidFileType')
}
let code: QRCode;
@ -33,11 +33,11 @@ export async function getPayloadBodyFromFile(file: File, color: string): Promise
inversionAttempts: "dontInvert",
});
} catch (e) {
throw Error("Could not decode QR Code from File");
throw Error('couldNotDecode');
}
if (code == undefined) {
throw Error("Could not find QR Code in provided File")
throw Error('couldNotFindQrCode')
}
// Get raw data
@ -49,7 +49,7 @@ export async function getPayloadBodyFromFile(file: File, color: string): Promise
try {
decodedData = decodeData(rawData);
} catch (e) {
throw Error("Invalid QR Code")
throw Error('invalidQrCode')
}
return {
@ -70,7 +70,7 @@ export async function getPayloadBodyFromQR(qrCodeResult: Result, color: string):
try {
decodedData = decodeData(rawData);
} catch (e) {
throw Error("Invalid QR Code")
throw Error("invalidQrCode")
}
return {

945
yarn.lock

File diff suppressed because it is too large Load Diff