feat: qr code uploading and decrypting
This commit is contained in:
parent
3df8a87c1c
commit
c9922481b1
5 changed files with 475 additions and 391 deletions
|
|
@ -12,8 +12,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "2.7.2",
|
"@auth/prisma-adapter": "2.7.2",
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
|
"@trafficlunar/asmcrypto.js": "^1.0.2",
|
||||||
"@yudiel/react-qr-scanner": "2.2.2-beta.2",
|
"@yudiel/react-qr-scanner": "2.2.2-beta.2",
|
||||||
"embla-carousel-react": "^8.5.2",
|
"embla-carousel-react": "^8.5.2",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
|
||||||
796
pnpm-lock.yaml
796
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
|
|
@ -8,6 +8,10 @@ import TagSelector from "./submit/tag-selector";
|
||||||
import QrUpload from "./submit/qr-upload";
|
import QrUpload from "./submit/qr-upload";
|
||||||
import QrScanner from "./submit/qr-scanner";
|
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() {
|
export default function SubmitForm() {
|
||||||
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
|
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
|
||||||
accept: {
|
accept: {
|
||||||
|
|
@ -18,6 +22,25 @@ export default function SubmitForm() {
|
||||||
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
|
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
|
||||||
const [qrBytes, setQrBytes] = useState<Uint8Array>(new Uint8Array());
|
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 (
|
return (
|
||||||
<form onSubmit={(e) => e.preventDefault()} className="grid grid-cols-2">
|
<form onSubmit={(e) => e.preventDefault()} className="grid grid-cols-2">
|
||||||
<div className="p-4">
|
<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">
|
<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>
|
<legend className="px-2">QR Code</legend>
|
||||||
|
|
||||||
<QrUpload />
|
<QrUpload setQrBytes={setQrBytes} />
|
||||||
|
|
||||||
<span>or</span>
|
<span>or</span>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytes }: Props) {
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleScan = (result: IDetectedBarcode[]) => {
|
const handleScan = (result: IDetectedBarcode[]) => {
|
||||||
|
// todo: fix scan, use jsQR instead, data is wrong
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
|
// Convert to bytes
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const byteArray = encoder.encode(result[0].rawValue);
|
const byteArray = encoder.encode(result[0].rawValue);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,43 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { FileWithPath, useDropzone } from "react-dropzone";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import jsQR from "jsqr";
|
||||||
|
|
||||||
export default function QrUpload() {
|
interface Props {
|
||||||
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
|
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: {
|
accept: {
|
||||||
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"],
|
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue