Files
no-whatever/src/lib/shareImage.ts
T
kurihada ce76980fe5 refactor(P0): JWT 认证、并发安全、错误日志三项安全加固
- 新增 JWT httpOnly cookie 认证链路 (jose),登录/注册签发 token,
  所有用户和盲盒 API 改为从 cookie 提取 userId,不再信任客户端传值
- 新增 /api/auth/logout 端点清除认证 cookie
- GET /api/user 区分 owner/非 owner,非 owner 不暴露 email
- atomicUpdateRoom 新增 per-room 应用层互斥锁,防止 SQLite 下并发 lost update
- 修复 getRoomData 中 fire-and-forget delete 改为 await
- 37 个静默 catch 块跨 17 个文件添加 console.error 日志
- 新增 REFACTOR_PLAN.md 全景分析文档
2026-03-02 17:24:26 +08:00

54 lines
1.5 KiB
TypeScript

import { toPng } from "html-to-image";
export async function loadImageAsDataUrl(src: string): Promise<string | null> {
try {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = src;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject();
});
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0);
return canvas.toDataURL("image/jpeg", 0.85);
} catch (e) {
console.error("proxyToDataUrl failed:", e);
return null;
}
}
export async function generateImage(el: HTMLElement): Promise<string> {
return toPng(el, {
pixelRatio: 2,
quality: 0.95,
cacheBust: true,
skipAutoScale: true,
filter: (node) => {
if (node instanceof HTMLElement && node.dataset.shareExclude === "true") {
return false;
}
return true;
},
});
}
export function downloadDataUrl(dataUrl: string, filename: string) {
const link = document.createElement("a");
link.download = filename;
link.href = dataUrl;
link.click();
}
export function dataUrlToFile(dataUrl: string, filename: string): File {
const arr = dataUrl.split(",");
const mime = arr[0].match(/:(.*?);/)?.[1] || "image/png";
const bstr = atob(arr[1]);
const u8 = new Uint8Array(bstr.length);
for (let i = 0; i < bstr.length; i++) u8[i] = bstr.charCodeAt(i);
return new File([u8], filename, { type: mime });
}