fix(web): 内网 http 下写请求与复制邀请链接都会失败
CI / backend (push) Canceled after 0s

crypto.randomUUID() 与 navigator.clipboard 只在安全上下文(https / localhost)里存在,
生产入口是纯 http,于是:

- 所有 POST/PATCH/DELETE 在拼 Idempotency-Key 时直接抛 TypeError,
  登录页只显示"登录失败",GET 一切正常,所以这个坑一直没暴露
- 「复制邀请链接」永远回"浏览器不允许自动复制"

改成幂等键自己拼一个 v4(getRandomValues 不受安全上下文限制,退回 Math.random),
复制走 navigator.clipboard → document.execCommand("copy") 两级降级。

顺带说明:这是生产上"能看不能写"的真正原因,不是权限或接口问题。
This commit is contained in:
Steven
2026-10-31 04:13:13 +08:00
parent 096acb00e7
commit e3f1310cff
4 changed files with 121 additions and 8 deletions
+25 -2
View File
@@ -48,6 +48,29 @@ export const session = {
export const authExpired = new EventTarget();
/**
* 幂等键。
*
* 别直接用 `crypto.randomUUID()`:它只在**安全上下文**(https 或 localhost)里存在,
* 而内网生产入口是纯 http,那里 `crypto.randomUUID` 是 undefined,调它会把
* 所有写请求变成 "登录失败" 这种莫名其妙的报错——GET 没事,所以特别难发现。
* `getRandomValues` 不受这个限制,退回它自己拼一个 v4。
*/
function newIdempotencyKey(): string {
const c = globalThis.crypto;
if (typeof c?.randomUUID === "function") return c.randomUUID();
const bytes = new Uint8Array(16);
if (typeof c?.getRandomValues === "function") {
c.getRandomValues(bytes);
} else {
for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
}
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
type Method = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
interface RequestOptions {
@@ -68,7 +91,7 @@ async function raw<T>(path: string, opts: RequestOptions = {}): Promise<T> {
const isWrite = method !== "GET";
if (isWrite) {
headers["Idempotency-Key"] = opts.idempotencyKey ?? crypto.randomUUID();
headers["Idempotency-Key"] = opts.idempotencyKey ?? newIdempotencyKey();
}
const resp = await fetch(`${BASE}${path}`, {
@@ -118,7 +141,7 @@ export async function uploadFile<T>(path: string, file: File): Promise<T> {
const headers: Record<string, string> = { Accept: "application/json" };
const token = session.access;
if (token) headers.Authorization = `Bearer ${token}`;
headers["Idempotency-Key"] = crypto.randomUUID();
headers["Idempotency-Key"] = newIdempotencyKey();
const resp = await fetch(`${BASE}${path}`, { method: "POST", headers, body: form });