From 3c09e11c3a36f3105920448df4f0fff964cdd8c1 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 12 Apr 2026 21:27:12 +0800 Subject: [PATCH] feat(server): errors, rate limit stub, verification policy, noop mailer Made-with: Cursor --- server/utils/errors.ts | 9 +++++++++ server/utils/mailer.ts | 9 +++++++++ server/utils/rate-limit.ts | 22 ++++++++++++++++++++++ server/utils/verification-policy.ts | 5 +++++ test/unit/verification-policy.test.ts | 11 +++++++++++ 5 files changed, 56 insertions(+) create mode 100644 server/utils/errors.ts create mode 100644 server/utils/mailer.ts create mode 100644 server/utils/rate-limit.ts create mode 100644 server/utils/verification-policy.ts create mode 100644 test/unit/verification-policy.test.ts diff --git a/server/utils/errors.ts b/server/utils/errors.ts new file mode 100644 index 0000000..890015c --- /dev/null +++ b/server/utils/errors.ts @@ -0,0 +1,9 @@ +import { createError } from "h3"; + +/** 与规格一致的错误体;依赖 Nitro 将 `createError` 的 `data` 序列化进 JSON。 */ +export function jsonError(status: number, code: string, message: string): never { + throw createError({ + statusCode: status, + data: { error: { code, message } }, + }); +} diff --git a/server/utils/mailer.ts b/server/utils/mailer.ts new file mode 100644 index 0000000..6cf9e56 --- /dev/null +++ b/server/utils/mailer.ts @@ -0,0 +1,9 @@ +export type Mailer = { + sendVerificationEmail(input: { to: string; token: string }): Promise; + sendPasswordResetEmail(input: { to: string; token: string }): Promise; +}; + +export const noopMailer: Mailer = { + async sendVerificationEmail() {}, + async sendPasswordResetEmail() {}, +}; diff --git a/server/utils/rate-limit.ts b/server/utils/rate-limit.ts new file mode 100644 index 0000000..c906e0f --- /dev/null +++ b/server/utils/rate-limit.ts @@ -0,0 +1,22 @@ +import { createError } from "h3"; +import { getRedis } from "./redis"; + +export async function rateLimitOrThrow( + key: string, + limit: number, + windowSeconds: number, +): Promise { + const redis = getRedis(); + const n = await redis.incr(key); + if (n === 1) { + await redis.expire(key, windowSeconds); + } + if (n > limit) { + throw createError({ + statusCode: 429, + data: { + error: { code: "RATE_LIMITED", message: "请求过于频繁,请稍后再试" }, + }, + }); + } +} diff --git a/server/utils/verification-policy.ts b/server/utils/verification-policy.ts new file mode 100644 index 0000000..d26330d --- /dev/null +++ b/server/utils/verification-policy.ts @@ -0,0 +1,5 @@ +const NEEDS_VERIFIED = new Set(["patch-me"]); + +export function needsEmailVerified(handlerId: string): boolean { + return NEEDS_VERIFIED.has(handlerId); +} diff --git a/test/unit/verification-policy.test.ts b/test/unit/verification-policy.test.ts new file mode 100644 index 0000000..8aac223 --- /dev/null +++ b/test/unit/verification-policy.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "bun:test"; +import { needsEmailVerified } from "../../server/utils/verification-policy"; + +describe("verification-policy", () => { + it("patch-me requires verified", () => { + expect(needsEmailVerified("patch-me")).toBe(true); + }); + it("unknown defaults false", () => { + expect(needsEmailVerified("other")).toBe(false); + }); +});