diff --git a/OpenElection.Booth/ApiModels/VerificationRequestModel.cs b/OpenElection.Booth/ApiModels/VerificationRequestModel.cs new file mode 100644 index 0000000..a17c9f2 --- /dev/null +++ b/OpenElection.Booth/ApiModels/VerificationRequestModel.cs @@ -0,0 +1,6 @@ +namespace OpenElection.Booth.ApiModels; + +public class VerificationRequestModel +{ + public string Hash { get; set; } +} \ No newline at end of file diff --git a/OpenElection.Booth/Controllers/VerificationController.cs b/OpenElection.Booth/Controllers/VerificationController.cs index 6e8c796..cf37404 100644 --- a/OpenElection.Booth/Controllers/VerificationController.cs +++ b/OpenElection.Booth/Controllers/VerificationController.cs @@ -8,9 +8,9 @@ namespace OpenElection.Booth.Controllers; public class VerificationController: ControllerBase { [HttpPost] - public IActionResult Verify(string hash) + public IActionResult Verify(VerificationRequestModel model) { - var successModel = new VerificationSuccessResponseModel("V123456", "John Doe", hash, true); + var successModel = new VerificationSuccessResponseModel("V123456", "John Doe", model.Hash, true); var failureModel = new VerificationFailureResponseModel("Invalid identity hash."); var random = new Random(); diff --git a/openelection.booth.web/package-lock.json b/openelection.booth.web/package-lock.json index f2b512a..806fc96 100644 --- a/openelection.booth.web/package-lock.json +++ b/openelection.booth.web/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-slot": "^1.2.4", "axios": "^1.13.2", "class-variance-authority": "0.7.1", + "html5-qrcode": "^2.3.8", "jsqr": "^1.4.0", "lucide-react": "0.487.0", "react": "^19.2.0", diff --git a/openelection.booth.web/package.json b/openelection.booth.web/package.json index 6e24af3..8c231b4 100644 --- a/openelection.booth.web/package.json +++ b/openelection.booth.web/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-slot": "^1.2.4", "axios": "^1.13.2", "class-variance-authority": "0.7.1", + "html5-qrcode": "^2.3.8", "jsqr": "^1.4.0", "lucide-react": "0.487.0", "react": "^19.2.0", diff --git a/openelection.booth.web/src/components/QRCodeScanner.tsx b/openelection.booth.web/src/components/QRCodeScanner.tsx index eb8b1ea..9679787 100644 --- a/openelection.booth.web/src/components/QRCodeScanner.tsx +++ b/openelection.booth.web/src/components/QRCodeScanner.tsx @@ -1,26 +1,124 @@ -import {useEffect} from "react"; +import {useEffect, useRef, useCallback} from "react"; import {useVerification} from "../contexts/VerificationContext"; -import {Html5QrcodeScanner} from "html5-qrcode"; +import {Html5Qrcode} from "html5-qrcode"; +import {Upload} from "lucide-react"; +import jsQR from "jsqr"; export function QRCodeScanner() { const {onQRCodeScanned} = useVerification(); + const scannerRef = useRef(null); + const isRunningRef = useRef(false); + const fileInputRef = useRef(null); - useEffect(() => { - let html5QrcodeScanner = new Html5QrcodeScanner( - "reader", - {fps: 10, qrbox: {width: 250, height: 250}}, - /* verbose= */ false); - html5QrcodeScanner.render(onQRCodeScanned, () => { - alert('Scan failed, please try again.') - }); + const startScanner = useCallback(async () => { + if (isRunningRef.current || scannerRef.current?.getState() === 2) { + return; // Already running + } + + if (!scannerRef.current) { + scannerRef.current = new Html5Qrcode("reader"); + } + + const scanner = scannerRef.current; + const config = {fps: 10, qrbox: {width: 250, height: 250}}; + + try { + await scanner.start( + {facingMode: "environment"}, + config, + (decodedText) => { + alert('QR Code scanned: ' + decodedText); + onQRCodeScanned(decodedText); + }, + (errorMessage) => { + console.debug(`QR Code scan error: ${errorMessage}`); + } + ); + isRunningRef.current = true; + console.log("QR Code scanner started successfully"); + } catch (error) { + console.error("Failed to start QR Code scanner:", error); + isRunningRef.current = false; + } + }, [onQRCodeScanned]); + + const handleFileSelect = useCallback(async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + alert("Failed to create canvas context"); + return; + } + + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, canvas.width, canvas.height); + + if (code) { + onQRCodeScanned(code.data); + } else { + alert("No QR code found in the selected image. Please try another image."); + } + }; + img.onerror = () => { + alert("Failed to load the image. Please try another image."); + }; + img.src = URL.createObjectURL(file); + } catch (error) { + console.error("Error scanning file:", error); + } }, [onQRCodeScanned]); + useEffect(() => { + startScanner(); + + return () => { + const scanner = scannerRef.current; + if (scanner && isRunningRef.current) { + scanner.stop() + .then(() => { + isRunningRef.current = false; + console.log("QR Code scanner stopped"); + }) + .catch(err => { + console.warn("Error stopping scanner:", err); + isRunningRef.current = false; + }); + } + } + }, [startScanner]); + return (

Position your QR code in front of the camera

+ +
+ + +
); } diff --git a/openelection.booth.web/src/contexts/VerificationContext.tsx b/openelection.booth.web/src/contexts/VerificationContext.tsx index 4908758..19d5c1a 100644 --- a/openelection.booth.web/src/contexts/VerificationContext.tsx +++ b/openelection.booth.web/src/contexts/VerificationContext.tsx @@ -1,4 +1,5 @@ import React, {createContext, useContext, useState, useCallback, useEffect} from "react"; +import {verifyCredentials} from "../services/credential-verification-service.ts"; interface VerificationContextType { isVerified: boolean; @@ -31,8 +32,9 @@ export function VerificationProvider({children}: { children: React.ReactNode }) const handleOnQRCodeScanned = useCallback((qrData: string) => { setIsScanning(true); - // TODO: Implement QR code verification API call - console.log("QR Code scanned:", qrData); + verifyCredentials(qrData).then((result) => { + setIsVerified(result.isVerified) + }).finally(()=>setIsScanning(false)); }, []); // TODO: Add diff --git a/openelection.booth.web/src/services/axios-service.ts b/openelection.booth.web/src/services/axios-service.ts new file mode 100644 index 0000000..ccfbfb7 --- /dev/null +++ b/openelection.booth.web/src/services/axios-service.ts @@ -0,0 +1,10 @@ +import * as axios from 'axios'; + +const http = axios.default.create({ + baseURL: '/', + headers: { + 'Content-Type': 'application/json', + } +}); + +export default http; diff --git a/openelection.booth.web/src/services/credential-verification-service.ts b/openelection.booth.web/src/services/credential-verification-service.ts new file mode 100644 index 0000000..8074079 --- /dev/null +++ b/openelection.booth.web/src/services/credential-verification-service.ts @@ -0,0 +1,21 @@ +import type {VerificationMessage} from "../models/verification-status-model.ts"; + +export const verifyCredentials = async (hash: string): Promise => { + try { + const response = await fetch('/api/verification', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ hash }), + credentials: 'include', + }); + if (!response.ok) { + throw new Error(`Server responded with status ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Error verifying credentials:', error); + throw error; + } +} \ No newline at end of file