Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions OpenElection.Booth/ApiModels/VerificationRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OpenElection.Booth.ApiModels;

public class VerificationRequestModel
{
public string Hash { get; set; }
}
4 changes: 2 additions & 2 deletions OpenElection.Booth/Controllers/VerificationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions openelection.booth.web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions openelection.booth.web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
118 changes: 108 additions & 10 deletions openelection.booth.web/src/components/QRCodeScanner.tsx
Original file line number Diff line number Diff line change
@@ -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<Html5Qrcode | null>(null);
const isRunningRef = useRef(false);
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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 (
<div className="space-y-4">
<div id="reader" style={{maxWidth: '100%'}}></div>
<p className="text-sm text-center text-gray-600">
Position your QR code in front of the camera
</p>

<div className="flex flex-col gap-3">
<button
onClick={() => fileInputRef.current?.click()}
className="flex items-center justify-center gap-2 w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium"
>
<Upload className="w-4 h-4" />
Upload from Gallery
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
</div>
</div>
);
}
Expand Down
6 changes: 4 additions & 2 deletions openelection.booth.web/src/contexts/VerificationContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions openelection.booth.web/src/services/axios-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as axios from 'axios';

const http = axios.default.create({
baseURL: '/',
headers: {
'Content-Type': 'application/json',
}
});

export default http;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {VerificationMessage} from "../models/verification-status-model.ts";

export const verifyCredentials = async (hash: string): Promise<VerificationMessage> => {
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;
}
}
Loading