/** * 设置路由(v2.1.0): * GET /api/settings 读取:全局 Webhook / API Key 元信息 / 当前用户 * PUT /api/settings 保存:全局 Webhook * POST /api/settings/api-key 生成(轮换)单一 API/MCP Key,明文仅返回一次 * DEL /api/settings/api-key 撤销并删除 API Key * PUT /api/settings/password 修改登录密码 * API 与 MCP 共用同一 Key,凭据存 settings 表(不再单独建表)。 */ import { Hono } from "hono"; import type { Env, AppVars } from "../env.ts"; import { requireSession } from "../middleware.ts"; import { checkWebhookUrl, generateWmailKey, hashPassword, isValidWmailKey, verifyPassword, } from "../../../shared/src/index.ts"; import { getSetting, keyHash, setSetting } from "../db.ts"; import { getGlobalWebhookKey, normalizeWebhookHeaders, saveGlobalWebhook, } from "../services/webhook_delivery.ts"; import { nowIso, readJson } from "../util.ts"; type App = Hono<{ Bindings: Env; Variables: AppVars }>; /** # 函数 * 读取 API Key 元信息(只有 last4,无明文) */ async function apiKeyMeta(env: Env): Promise<{ has_key: boolean; key_last4: string; created_at: string | null }> { const [last4, createdAt] = await Promise.all([ getSetting(env, "api_key_last4"), getSetting(env, "api_key_created_at"), ]); return { has_key: Boolean(last4), key_last4: last4 ?? "", created_at: createdAt ?? null }; } /** # 函数 * 装配设置路由(会话保护) */ export function settingsRoutes(): App { const app = new Hono<{ Bindings: Env; Variables: AppVars }>(); app.use("*", requireSession); /** 读取设置(key 只回 last4,不回明文) */ app.get("/", async (c) => { const userId = c.get("userId") ?? ""; const user = await c.env.DB.prepare("SELECT username FROM users WHERE id = ?").bind(userId).first<{ username: string }>(); const raw = await getSetting(c.env, "global_webhook"); let webhook: Record = { url: "", headers: {}, has_key: false, key_last4: "" }; if (raw) { try { const cfg = JSON.parse(raw) as { url?: string; headers?: Record }; const key = await getGlobalWebhookKey(c.env); webhook = { url: cfg.url ?? "", headers: cfg.headers ?? {}, has_key: Boolean(key), key_last4: key.slice(-4), }; } catch { // settings 损坏时按未配置处理 } } return c.json({ username: user?.username ?? "", webhook, api_key: await apiKeyMeta(c.env), }); }); /** 保存设置:全局 Webhook */ app.put("/", async (c) => { const body = await readJson<{ webhook?: { url?: string; headers?: unknown; key?: string }; }>(c.req.raw); if (body.webhook) { const url = (body.webhook.url ?? "").trim(); let headers: Record; try { headers = normalizeWebhookHeaders(body.webhook.headers); } catch (err) { return c.json({ error: err instanceof Error ? err.message : "invalid headers" }, 400); } if (url) { const unsafe = checkWebhookUrl(url, c.env.WMAILHUB_ALLOW_PRIVATE_WEBHOOKS === "1"); if (unsafe) return c.json({ error: unsafe }, 400); } // key:undefined 保留现有;空白串(含 "")清除;非空设置新 key const rawKey = typeof body.webhook.key === "string" ? body.webhook.key : undefined; await saveGlobalWebhook(c.env, { url, headers, key: rawKey !== undefined && rawKey.trim() === "" ? "" : rawKey, }); } return c.json({ ok: true }); }); /** 生成(轮换)API/MCP Key:明文仅本次返回;旧 Key 立即失效 */ app.post("/api-key", async (c) => { const key = generateWmailKey(); if (!isValidWmailKey(key)) return c.json({ error: "key generator broken" }, 500); const createdAt = nowIso(); await setSetting(c.env, "api_key_hash", await keyHash(key)); await setSetting(c.env, "api_key_last4", key.slice(-4)); await setSetting(c.env, "api_key_created_at", createdAt); return c.json({ ok: true, key, created_at: createdAt }, 201); }); /** 撤销并删除 API Key */ app.delete("/api-key", async (c) => { for (const k of ["api_key_hash", "api_key_last4", "api_key_created_at"]) { await c.env.DB.prepare("DELETE FROM settings WHERE key = ?").bind(k).run(); } return c.json({ ok: true }); }); /** 修改登录密码 */ app.put("/password", async (c) => { const body = await readJson<{ old_password?: string; new_password?: string }>(c.req.raw); const oldPassword = body.old_password ?? ""; const newPassword = body.new_password ?? ""; if (newPassword.length < 6) return c.json({ error: "新密码至少 6 位" }, 400); const userId = c.get("userId") ?? ""; const user = await c.env.DB.prepare("SELECT id, password_hash FROM users WHERE id = ?") .bind(userId) .first<{ id: string; password_hash: string }>(); if (!user) return c.json({ error: "user not found" }, 404); if (!(await verifyPassword(oldPassword, user.password_hash))) { return c.json({ error: "原密码不正确" }, 400); } await c.env.DB.prepare("UPDATE users SET password_hash = ? WHERE id = ?") .bind(await hashPassword(newPassword), userId) .run(); return c.json({ ok: true }); }); return app; }