feat: qr code uploading and decrypting

This commit is contained in:
trafficlunar 2025-04-03 21:10:16 +01:00
parent 3df8a87c1c
commit c9922481b1
5 changed files with 475 additions and 391 deletions

View file

@ -12,8 +12,10 @@
"dependencies": {
"@auth/prisma-adapter": "2.7.2",
"@prisma/client": "^6.5.0",
"@trafficlunar/asmcrypto.js": "^1.0.2",
"@yudiel/react-qr-scanner": "2.2.2-beta.2",
"embla-carousel-react": "^8.5.2",
"jsqr": "^1.4.0",
"next": "15.2.4",
"next-auth": "5.0.0-beta.25",
"react": "^19.0.0",

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useDropzone } from "react-dropzone";
import { Icon } from "@iconify/react";
@ -8,6 +8,10 @@ import TagSelector from "./submit/tag-selector";
import QrUpload from "./submit/qr-upload";
import QrScanner from "./submit/qr-scanner";
import { AES_CCM } from "@trafficlunar/asmcrypto.js";
const key = new Uint8Array([0x59, 0xfc, 0x81, 0x7e, 0x64, 0x46, 0xea, 0x61, 0x90, 0x34, 0x7b, 0x20, 0xe9, 0xbd, 0xce, 0x52]);
export default function SubmitForm() {
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
accept: {
@ -18,6 +22,25 @@ export default function SubmitForm() {
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
const [qrBytes, setQrBytes] = useState<Uint8Array>(new Uint8Array());
useEffect(() => {
if (qrBytes.length == 0) return;
const decrypt = async () => {
const nonce = qrBytes.subarray(0, 8);
const content = qrBytes.subarray(8, 0x70);
const nonceWithZeros = new Uint8Array(12);
nonceWithZeros.set(nonce, 0);
const decrypted = AES_CCM.decrypt(content, key, nonceWithZeros, undefined, 16);
const result = new Uint8Array([...decrypted.subarray(0, 12), ...qrBytes.subarray(0, 8), ...decrypted.subarray(12, decrypted.length - 4)]);
console.log(result);
};
decrypt();
}, [qrBytes]);
return (
<form onSubmit={(e) => e.preventDefault()} className="grid grid-cols-2">
<div className="p-4">
@ -66,7 +89,7 @@ export default function SubmitForm() {
<fieldset className="border-t-2 border-b-2 border-black p-3 flex flex-col items-center gap-2">
<legend className="px-2">QR Code</legend>
<QrUpload />
<QrUpload setQrBytes={setQrBytes} />
<span>or</span>

View file

@ -25,8 +25,10 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytes }: Props) {
}, [isOpen]);
const handleScan = (result: IDetectedBarcode[]) => {
// todo: fix scan, use jsQR instead, data is wrong
setIsOpen(false);
// Convert to bytes
const encoder = new TextEncoder();
const byteArray = encoder.encode(result[0].rawValue);

View file

@ -1,10 +1,43 @@
"use client";
import { useCallback } from "react";
import { FileWithPath, useDropzone } from "react-dropzone";
import { Icon } from "@iconify/react";
import { useDropzone } from "react-dropzone";
import jsQR from "jsqr";
export default function QrUpload() {
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
interface Props {
setQrBytes: React.Dispatch<React.SetStateAction<Uint8Array>>;
}
export default function QrUpload({ setQrBytes }: Props) {
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
acceptedFiles.forEach((file) => {
// Scan QR code
const reader = new FileReader();
reader.onload = async (event) => {
const image = new Image();
image.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width, image.height);
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const decoded = jsQR(imageData.data, image.width, image.height);
setQrBytes(new Uint8Array(decoded?.binaryData!));
};
image.src = event.target!.result as string;
};
reader.readAsDataURL(file);
});
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: {
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"],
},