Merge pull request #19 from covidpass-org/dev
Added a build int QR-Code reader. Support upload QR-Codes as PNG
|
@ -23,8 +23,7 @@ RUN addgroup -g 1001 -S nodejs
|
||||||
RUN adduser -S nextjs -u 1001
|
RUN adduser -S nextjs -u 1001
|
||||||
|
|
||||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
# 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 ./
|
||||||
COPY --from=builder /app/server.js ./
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
|
|
||||||
Web app for adding EU COVID-19 Vaccination Certificates to your wallets
|
Web app for adding EU COVID-19 Vaccination Certificates to your wallets
|
||||||
|
|
||||||
The API can be found [here](https://github.com/marvinsxtr/covidpass-api).
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
export default Card
|
export default Card
|
||||||
|
|
||||||
function Card({ heading, content, step }) {
|
function Card({heading, content, step}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md p-6 bg-white dark:bg-gray-800 space-y-4">
|
<div className="rounded-md p-6 bg-white dark:bg-gray-800 space-y-4">
|
||||||
{step &&
|
{
|
||||||
<div className="flex flex-row items-center">
|
step &&
|
||||||
<div className="rounded-full p-4 bg-green-600 h-5 w-5 flex items-center justify-center">
|
<div className="flex flex-row items-center">
|
||||||
<p className="text-white text-lg font-bold">
|
<div className="rounded-full p-4 bg-green-600 h-5 w-5 flex items-center justify-center">
|
||||||
{step}
|
<p className="text-white text-lg font-bold">
|
||||||
</p>
|
{step}
|
||||||
</div>
|
</p>
|
||||||
<div className="ml-3 font-bold text-xl">
|
</div>
|
||||||
{heading}
|
<div className="ml-3 font-bold text-xl">
|
||||||
</div>
|
{heading}
|
||||||
</div>}
|
</div>
|
||||||
<div className="text-lg">
|
</div>
|
||||||
{content}
|
}
|
||||||
</div>
|
<div className="text-lg">
|
||||||
</div>
|
{content}
|
||||||
)
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -1,190 +1,304 @@
|
||||||
const PDFJS = require('pdfjs-dist')
|
|
||||||
PDFJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS.version}/pdf.worker.js`
|
|
||||||
|
|
||||||
import jsQR from "jsqr"
|
|
||||||
import {saveAs} from 'file-saver'
|
import {saveAs} from 'file-saver'
|
||||||
|
import {BrowserQRCodeReader} from '@zxing/browser'
|
||||||
|
import React, {useEffect, useRef, useState} from "react"
|
||||||
import {decodeData} from "../src/decode"
|
import {decodeData} from "../src/decode"
|
||||||
|
import {processPdf, processPng} from "../src/process"
|
||||||
import {createPass} from "../src/pass"
|
import {createPass} from "../src/pass"
|
||||||
import Card from "../components/Card"
|
import Card from "../components/Card"
|
||||||
import Alert from "../components/Alert"
|
import Alert from "../components/Alert"
|
||||||
|
import jsQR from "jsqr";
|
||||||
|
|
||||||
export default Form
|
export default Form
|
||||||
|
|
||||||
function Form() {
|
function Form() {
|
||||||
|
|
||||||
function readFileAsync(file) {
|
function readFileAsync(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = () => {
|
|
||||||
resolve(reader.result);
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.onerror = reject;
|
|
||||||
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = function(heading, message) {
|
reader.onload = () => {
|
||||||
const alert = document.getElementById('alert')
|
resolve(reader.result);
|
||||||
alert.setAttribute('style', null)
|
};
|
||||||
|
|
||||||
document.getElementById('heading').innerHTML = heading
|
|
||||||
document.getElementById('message').innerHTML = message
|
|
||||||
|
|
||||||
document.getElementById('spin').style.display = 'none'
|
reader.onerror = reject;
|
||||||
}
|
|
||||||
|
|
||||||
const processPdf = async function() {
|
reader.readAsArrayBuffer(file);
|
||||||
document.getElementById('spin').style.display = 'block'
|
})
|
||||||
|
|
||||||
const file = document.getElementById('pdf').files[0]
|
|
||||||
|
|
||||||
const result = await readFileAsync(file)
|
|
||||||
let typedArray = new Uint8Array(result)
|
|
||||||
|
|
||||||
const canvas = document.getElementById('canvas')
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
|
|
||||||
let loadingTask = PDFJS.getDocument(typedArray)
|
|
||||||
await loadingTask.promise.then(async function (pdfDocument) {
|
|
||||||
const pdfPage = await pdfDocument.getPage(1)
|
|
||||||
const viewport = pdfPage.getViewport({ scale: 1 })
|
|
||||||
canvas.width = viewport.width
|
|
||||||
canvas.height = viewport.height
|
|
||||||
|
|
||||||
const renderTask = pdfPage.render({
|
|
||||||
canvasContext: ctx,
|
|
||||||
viewport,
|
|
||||||
})
|
|
||||||
|
|
||||||
return await renderTask.promise
|
|
||||||
})
|
|
||||||
|
|
||||||
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
||||||
let code = jsQR(imageData.data, imageData.width, imageData.height, {
|
|
||||||
inversionAttempts: 'dontInvert',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
const rawData = code.data
|
|
||||||
let decoded
|
|
||||||
|
|
||||||
try {
|
|
||||||
decoded = decodeData(rawData)
|
|
||||||
} catch (error) {
|
|
||||||
error('Invalid QR code found', 'Make sure that you picked the correct PDF')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {decoded: decoded, raw: rawData}
|
|
||||||
} else {
|
|
||||||
error('No QR code found', 'Try scanning the PDF again')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addToWallet = async function(event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
let result
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await processPdf()
|
|
||||||
} catch {
|
|
||||||
error('Error:', 'Could not extract QR code data from PDF')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof result === 'undefined') {
|
const error = function (heading, message) {
|
||||||
return
|
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 color = document.getElementById('color').value
|
const processFile = async function () {
|
||||||
|
console.log(qrCode)
|
||||||
try {
|
console.log(file)
|
||||||
const pass = await createPass(
|
if (!qrCode && !file) {
|
||||||
{
|
error("Error", "Please capture a QR Code or select a file to scan");
|
||||||
decoded: result.decoded,
|
return;
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
document.getElementById('spin').style.display = 'block'
|
||||||
<div>
|
|
||||||
<form className="space-y-5" id="form" action="https://api.covidpass.marvinsextro.de/covid.pkpass" method="POST" onSubmit={(e) => addToWallet(e)}>
|
let rawData;
|
||||||
<Card step={1} heading="Select Certificate" content={
|
|
||||||
<div className="space-y-5">
|
if (file) {
|
||||||
<p>
|
let imageData
|
||||||
Please select the (scanned) certificate PDF page, which you received from your doctor, pharmacy, vaccination centre or online.
|
const fileBuffer = await readFileAsync(file)
|
||||||
</p>
|
|
||||||
<input
|
switch (file.type) {
|
||||||
className="w-full"
|
case 'application/pdf':
|
||||||
type="file"
|
console.log('pdf')
|
||||||
id="pdf"
|
imageData = await processPdf(fileBuffer)
|
||||||
accept="application/pdf"
|
break
|
||||||
required
|
case 'image/png':
|
||||||
/>
|
console.log('png')
|
||||||
</div>
|
imageData = await processPng(fileBuffer)
|
||||||
} />
|
break
|
||||||
<Card step={2} heading="Pick a Color" content={
|
default:
|
||||||
<div className="relative inline-block w-full">
|
error('Error', 'Invalid file type')
|
||||||
<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">
|
return
|
||||||
<option value="white">white</option>
|
}
|
||||||
<option value="black">black</option>
|
|
||||||
<option value="grey">grey</option>
|
let code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||||
<option value="green">green</option>
|
inversionAttempts: 'dontInvert',
|
||||||
<option value="indigo">indigo</option>
|
})
|
||||||
<option value="blue">blue</option>
|
|
||||||
<option value="purple">purple</option>
|
rawData = code.data;
|
||||||
<option value="teal">teal</option>
|
|
||||||
</select>
|
} else {
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
rawData = qrCode.getText()
|
||||||
<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>
|
if (rawData) {
|
||||||
} />
|
let decoded
|
||||||
<Card step={3} heading="Add to Wallet" content={
|
|
||||||
<div className="space-y-5">
|
try {
|
||||||
<p>
|
decoded = decodeData(rawData)
|
||||||
Data privacy is of special importance when processing health-related data.
|
} catch (e) {
|
||||||
In order for you to make an informed decision, please read the <a href="/privacy">Privacy Policy</a>.
|
error('Invalid QR code found', 'Try another method to select your certificate')
|
||||||
</p>
|
return;
|
||||||
<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>
|
return {decoded: decoded, raw: rawData}
|
||||||
I accept the <a href="/privacy" className="underline">Privacy Policy</a>
|
} else {
|
||||||
</p>
|
error('No QR code found', 'Try another method to select your certificate')
|
||||||
</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
|
const addToWallet = async function (event) {
|
||||||
</button>
|
event.preventDefault()
|
||||||
<div id="spin" style={{ "display": "none" }}>
|
|
||||||
<svg className="animate-spin h-5 w-5 ml-2" viewBox="0 0 24 24">
|
let result
|
||||||
<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"/>
|
try {
|
||||||
</svg>
|
result = await processFile()
|
||||||
</div>
|
} catch (e) {
|
||||||
</div>
|
error('Error:', 'Could not extract QR code data from certificate')
|
||||||
</div>
|
}
|
||||||
} />
|
|
||||||
</form>
|
if (typeof result === 'undefined') {
|
||||||
<Alert />
|
return
|
||||||
<canvas id="canvas" style={{ display: "none" }} />
|
}
|
||||||
</div>
|
|
||||||
)
|
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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
import Icon from '../public/favicon.svg'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default Logo
|
export default Logo
|
||||||
|
|
||||||
function Logo() {
|
function Logo() {
|
||||||
return (
|
return (
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="flex flex-row items-center p-3 justify-center space-x-1" >
|
<a className="flex flex-row items-center p-3 justify-center space-x-1">
|
||||||
<Icon className="fill-current" />
|
<svg className="fill-current" xmlns="http://www.w3.org/2000/svg" enableBackground="new 0 0 24 24"
|
||||||
<h1 className="text-3xl font-bold">
|
height="48px"
|
||||||
CovidPass
|
viewBox="0 0 24 24" width="48px" fill="#000000">
|
||||||
</h1>
|
<g>
|
||||||
</a>
|
<path d="M0,0h24v24H0V0z" fill="none"/>
|
||||||
</Link>
|
</g>
|
||||||
)
|
<g>
|
||||||
|
<path
|
||||||
|
d="M11.3,2.26l-6,2.25C4.52,4.81,4,5.55,4,6.39v4.71c0,5.05,3.41,9.76,8,10.91c4.59-1.15,8-5.86,8-10.91V6.39 c0-0.83-0.52-1.58-1.3-1.87l-6-2.25C12.25,2.09,11.75,2.09,11.3,2.26z M10.23,14.83l-2.12-2.12c-0.39-0.39-0.39-1.02,0-1.41l0,0 c0.39-0.39,1.02-0.39,1.41,0l1.41,1.41l3.54-3.54c0.39-0.39,1.02-0.39,1.41,0l0,0c0.39,0.39,0.39,1.02,0,1.41l-4.24,4.24 C11.26,15.22,10.62,15.22,10.23,14.83z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<h1 className="text-3xl font-bold">
|
||||||
|
CovidPass
|
||||||
|
</h1>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ function Page({ content }) {
|
||||||
<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">
|
<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>
|
<Head>
|
||||||
<title>CovidPass</title>
|
<title>CovidPass</title>
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div>
|
<div>
|
||||||
<main className="flex flex-col space-y-5">
|
<main className="flex flex-col space-y-5">
|
||||||
|
@ -18,11 +18,11 @@ function Page({ content }) {
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<nav className="nav flex space-x-4 m-6 flex-row-reverse space-x-reverse text-md font-bold">
|
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold">
|
||||||
<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" >Donate</a>
|
<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>
|
<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>
|
</nav>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -15,12 +15,12 @@ export default {
|
||||||
additionalLinkTags: [
|
additionalLinkTags: [
|
||||||
{
|
{
|
||||||
rel: 'icon',
|
rel: 'icon',
|
||||||
href: 'https://covidpass.marvinsextro.de/favicon.png',
|
href: 'https://covidpass.marvinsextro.de/favicon.ico',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: 'apple-touch-icon',
|
rel: 'apple-touch-icon',
|
||||||
href: 'https://covidpass.marvinsextro.de/favicon.png',
|
href: 'https://covidpass.marvinsextro.de/apple-touch-icon.png',
|
||||||
sizes: '96x96'
|
sizes: '180x180'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
webpack(config) {
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: ['@svgr/webpack'],
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -4,27 +4,30 @@
|
||||||
"author": "Marvin Sextro <marvin.sextro@gmail.com>",
|
"author": "Marvin Sextro <marvin.sextro@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node server.js",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "NODE_ENV=production node server.js"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@zxing/browser": "^0.0.9",
|
||||||
|
"@zxing/library": "^0.18.6",
|
||||||
"base45-js": "^1.0.1",
|
"base45-js": "^1.0.1",
|
||||||
"cbor-js": "^0.1.0",
|
"cbor-js": "^0.1.0",
|
||||||
"do-not-zip": "^1.0.0",
|
"do-not-zip": "^1.0.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"jpeg-js": "^0.4.3",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"next": "latest",
|
"next": "latest",
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"pdfjs-dist": "^2.5.207",
|
"pdfjs-dist": "^2.5.207",
|
||||||
|
"pngjs": "^6.0.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"webpack": "^5.0.0",
|
"webpack": "^5.0.0",
|
||||||
"worker-loader": "^3.0.7"
|
"worker-loader": "^3.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^5.5.0",
|
|
||||||
"autoprefixer": "^10.0.4",
|
"autoprefixer": "^10.0.4",
|
||||||
"postcss": "^8.1.10",
|
"postcss": "^8.1.10",
|
||||||
"tailwindcss": "^2.1.1"
|
"tailwindcss": "^2.1.1"
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
import Document, {Html, Head, Main, NextScript} from 'next/document'
|
||||||
|
|
||||||
class CustomDocument extends Document {
|
class CustomDocument extends Document {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="en">
|
||||||
<Head />
|
<Head/>
|
||||||
<body className="bg-gray-200 dark:bg-gray-900 text-gray-800 dark:text-white">
|
<body className="bg-gray-200 dark:bg-gray-900 text-gray-800 dark:text-white">
|
||||||
<Main />
|
<Main/>
|
||||||
<NextScript />
|
<NextScript/>
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,10 +17,16 @@ export default function Home() {
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: 'https://covidpass.marvinsextro.de/thumbnail.png',
|
url: 'https://covidpass.marvinsextro.de/thumbnail.png',
|
||||||
width: 611,
|
width: 1000,
|
||||||
height: 318,
|
height: 500,
|
||||||
alt: 'CovidPass: Add your EU Digital Covid Vaccination Certificates to your favorite wallet app.',
|
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',
|
site_name: 'CovidPass',
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default function Privacy() {
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<ul className="list-disc">
|
<ul className="list-disc">
|
||||||
<li>
|
<li>
|
||||||
The whole process of generating the pass file happens locally in your browser. For the signing step, ony a hashed representation of your data is sent to the server.
|
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>
|
||||||
<li>
|
<li>
|
||||||
Your data is not stored beyond the active browser session and the site does not use cookies.
|
Your data is not stored beyond the active browser session and the site does not use cookies.
|
||||||
|
@ -34,7 +34,7 @@ export default function Privacy() {
|
||||||
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>.
|
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>
|
||||||
<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>
|
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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,7 +78,7 @@ export default function Privacy() {
|
||||||
</p>
|
</p>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<ul className="list-disc">
|
<ul className="list-disc">
|
||||||
<li>Assembling the signed pass file out of the inclomplete file generated locally and the signature</li>
|
<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>Saving the file on your device</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 14 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/></g><g><path d="M11.3,2.26l-6,2.25C4.52,4.81,4,5.55,4,6.39v4.71c0,5.05,3.41,9.76,8,10.91c4.59-1.15,8-5.86,8-10.91V6.39 c0-0.83-0.52-1.58-1.3-1.87l-6-2.25C12.25,2.09,11.75,2.09,11.3,2.26z M10.23,14.83l-2.12-2.12c-0.39-0.39-0.39-1.02,0-1.41l0,0 c0.39-0.39,1.02-0.39,1.41,0l1.41,1.41l3.54-3.54c0.39-0.39,1.02-0.39,1.41,0l0,0c0.39,0.39,0.39,1.02,0,1.41l-4.24,4.24 C11.26,15.22,10.62,15.22,10.23,14.83z"/></g></svg>
|
|
Before Width: | Height: | Size: 588 B |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 52 KiB |
25
server.js
|
@ -1,25 +0,0 @@
|
||||||
const { createServer } = require('http')
|
|
||||||
const { parse } = require('url')
|
|
||||||
const next = require('next')
|
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production'
|
|
||||||
const app = next({ dev })
|
|
||||||
const handle = app.getRequestHandler()
|
|
||||||
|
|
||||||
app.prepare().then(() => {
|
|
||||||
createServer((req, res) => {
|
|
||||||
const parsedUrl = parse(req.url, true)
|
|
||||||
const { pathname, query } = parsedUrl
|
|
||||||
|
|
||||||
if (pathname === '/a') {
|
|
||||||
app.render(req, res, '/a', query)
|
|
||||||
} else if (pathname === '/b') {
|
|
||||||
app.render(req, res, '/b', query)
|
|
||||||
} else {
|
|
||||||
handle(req, res, parsedUrl)
|
|
||||||
}
|
|
||||||
}).listen(3000, (err) => {
|
|
||||||
if (err) throw err
|
|
||||||
console.log('> Ready on http://localhost:3000')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|