53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import QRCode from "qrcode";
|
|
import { env } from "@/lib/env";
|
|
|
|
/**
|
|
* Operation QR tokens are opaque, URL-safe, high-entropy identifiers
|
|
* printed on traveler cards. 24 bytes = 192 bits of entropy, encoded
|
|
* as base64url -> 32 characters.
|
|
*/
|
|
export function generateQrToken(): string {
|
|
return randomBytes(24)
|
|
.toString("base64")
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=+$/, "");
|
|
}
|
|
|
|
/**
|
|
* The payload baked into the QR image is a full absolute URL so any phone
|
|
* camera app (which opens scans in a browser) goes straight to the scan page.
|
|
* APP_URL must match the externally reachable origin — see docs/DEPLOY.md.
|
|
*/
|
|
export function scanUrlForToken(token: string): string {
|
|
const base = env.APP_URL.replace(/\/+$/, "");
|
|
return `${base}/op/scan/${token}`;
|
|
}
|
|
|
|
/**
|
|
* Render the scan URL as a data-URL PNG suitable for <img src=...> on the
|
|
* admin detail page and, later, on the printable traveler card. Error
|
|
* correction level M balances density against smudge tolerance on paper.
|
|
*/
|
|
export async function renderQrPng(token: string): Promise<string> {
|
|
return QRCode.toDataURL(scanUrlForToken(token), {
|
|
errorCorrectionLevel: "M",
|
|
margin: 1,
|
|
width: 256,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Same QR, but as a raw PNG buffer for embedding into pdf-lib documents.
|
|
* We bump width up so the vector→raster downscale at print time stays crisp;
|
|
* 512 px at 35 mm = ~370 dpi which beats any office printer we'll see.
|
|
*/
|
|
export async function renderQrPngBuffer(token: string, width = 512): Promise<Buffer> {
|
|
return QRCode.toBuffer(scanUrlForToken(token), {
|
|
errorCorrectionLevel: "M",
|
|
margin: 1,
|
|
width,
|
|
});
|
|
}
|