Dev/v1.0 (#1)
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local> Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import QRCode from 'qrcode';
|
||||
import { CUSTOMER_APP_URL } from '../constants.js';
|
||||
|
||||
const DEFAULT_QR_SECRET = 'qms-branch-qr-v1';
|
||||
|
||||
function getQrSecret() {
|
||||
return import.meta.env.VITE_QR_TOKEN_SECRET || DEFAULT_QR_SECRET;
|
||||
}
|
||||
|
||||
function toBase64Url(bytes) {
|
||||
let binary = '';
|
||||
bytes.forEach((byte) => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function fromBase64Url(token) {
|
||||
const padded = token.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padLength = (4 - (padded.length % 4)) % 4;
|
||||
const base64 = padded + '='.repeat(padLength);
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function xorBytes(bytes, secret) {
|
||||
const key = new TextEncoder().encode(secret);
|
||||
return bytes.map((byte, index) => byte ^ key[index % key.length]);
|
||||
}
|
||||
|
||||
/** Opaque token so QR URLs don't expose tenant code / branch id. */
|
||||
export function encodeBranchQrToken(tenantCode, branchId, secret = getQrSecret()) {
|
||||
const plain = `v1:${String(tenantCode).trim().toUpperCase()}:${Number(branchId)}`;
|
||||
const plainBytes = new TextEncoder().encode(plain);
|
||||
return toBase64Url(xorBytes(plainBytes, secret));
|
||||
}
|
||||
|
||||
export function decodeBranchQrToken(token, secret = getQrSecret()) {
|
||||
try {
|
||||
const plain = new TextDecoder().decode(xorBytes(fromBase64Url(token), secret));
|
||||
const match = /^v1:([^:]+):(\d+)$/.exec(plain);
|
||||
if (!match) return null;
|
||||
return {
|
||||
tenantCode: match[1],
|
||||
branchId: Number(match[2]),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBranchTicketUrl(tenantCode, branchId) {
|
||||
const base = CUSTOMER_APP_URL.replace(/\/$/, '');
|
||||
const token = encodeBranchQrToken(tenantCode, branchId);
|
||||
return `${base}/q/${token}`;
|
||||
}
|
||||
|
||||
export async function generateBranchQrDataUrl(tenantCode, branchId) {
|
||||
return QRCode.toDataURL(getBranchTicketUrl(tenantCode, branchId), {
|
||||
errorCorrectionLevel: 'M',
|
||||
margin: 2,
|
||||
width: 1024,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function slugify(value) {
|
||||
return String(value)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '') || 'branch';
|
||||
}
|
||||
|
||||
export function downloadDataUrl(dataUrl, filename) {
|
||||
const link = document.createElement('a');
|
||||
link.href = dataUrl;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
|
||||
export async function downloadBranchQr({ tenantCode, branchId, branchName }) {
|
||||
const dataUrl = await generateBranchQrDataUrl(tenantCode, branchId);
|
||||
downloadDataUrl(dataUrl, `qr-${slugify(branchName)}-${branchId}.png`);
|
||||
return dataUrl;
|
||||
}
|
||||
Reference in New Issue
Block a user