Files
wmailhub/console/src/routes/settings.ts
T
bigcode 7370cce784 feat: v2.1.0 界面与凭据改进 —— 单一 API/MCP Key、邮件 Markdown 正文、/inboxs 域名筛选、暗色主题
- 单一 Key:API 与 MCP 共用一个 wmh_ Key,凭据迁移至 settings 表(0004 删除 api_credentials)
- Markdown:入库时 HTML→Markdown(mailhook 同思路),webhook payload 新增 markdown 字段
- /emails 路由改为 /inboxs 并支持域名筛选;设置按钮移入头像下拉
- 主题切换按钮(头像左侧,亮/暗持久化);设置模态仅 全局 Webhook/账号密码 两 tab 且高度限屏
- 列表操作列按钮加 SVG 图标;mailboxes 去 Webhook 列;最近邮件加收件人列
2026-09-30 13:27:28 +08:00

147 lines
5.3 KiB
TypeScript

/**
* 设置路由(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<string, unknown> = { url: "", headers: {}, has_key: false, key_last4: "" };
if (raw) {
try {
const cfg = JSON.parse(raw) as { url?: string; headers?: Record<string, string> };
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<string, string>;
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;
}