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); + }); +});