mirror of
https://github.com/beilunyang/moemail.git
synced 2026-07-12 16:11:20 +08:00
feat(turnstile): integrate Cloudflare Turnstile for enhanced security in login and registration processes
This commit is contained in:
@@ -8,9 +8,10 @@ import { getRequestContext } from "@cloudflare/next-on-pages"
|
||||
import { Permission, hasPermission, ROLES, Role } from "./permissions"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import { hashPassword, comparePassword } from "@/lib/utils"
|
||||
import { authSchema } from "@/lib/validation"
|
||||
import { authSchema, AuthSchema } from "@/lib/validation"
|
||||
import { generateAvatarUrl } from "./avatar"
|
||||
import { getUserId } from "./apiKey"
|
||||
import { verifyTurnstileToken } from "./turnstile"
|
||||
|
||||
const ROLE_DESCRIPTIONS: Record<Role, string> = {
|
||||
[ROLES.EMPEROR]: "皇帝(网站所有者)",
|
||||
@@ -113,26 +114,35 @@ export const {
|
||||
throw new Error("请输入用户名和密码")
|
||||
}
|
||||
|
||||
const { username, password } = credentials
|
||||
const { username, password, turnstileToken } = credentials as Record<string, string | undefined>
|
||||
|
||||
let parsedCredentials: AuthSchema
|
||||
try {
|
||||
authSchema.parse({ username, password })
|
||||
parsedCredentials = authSchema.parse({ username, password, turnstileToken })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
throw new Error("输入格式不正确")
|
||||
}
|
||||
|
||||
const verification = await verifyTurnstileToken(parsedCredentials.turnstileToken)
|
||||
if (!verification.success) {
|
||||
if (verification.reason === "missing-token") {
|
||||
throw new Error("请先完成安全验证")
|
||||
}
|
||||
throw new Error("安全验证未通过")
|
||||
}
|
||||
|
||||
const db = createDb()
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.username, username as string),
|
||||
where: eq(users.username, parsedCredentials.username),
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error("用户名或密码错误")
|
||||
}
|
||||
|
||||
const isValid = await comparePassword(password as string, user.password as string)
|
||||
const isValid = await comparePassword(parsedCredentials.password, user.password as string)
|
||||
if (!isValid) {
|
||||
throw new Error("用户名或密码错误")
|
||||
}
|
||||
|
||||
65
app/lib/turnstile.ts
Normal file
65
app/lib/turnstile.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getRequestContext } from "@cloudflare/next-on-pages"
|
||||
|
||||
interface TurnstileConfig {
|
||||
enabled: boolean
|
||||
siteKey: string
|
||||
secretKey: string
|
||||
}
|
||||
|
||||
export async function getTurnstileConfig(): Promise<TurnstileConfig> {
|
||||
const env = getRequestContext().env
|
||||
const [enabled, siteKey, secretKey] = await Promise.all([
|
||||
env.SITE_CONFIG.get("TURNSTILE_ENABLED"),
|
||||
env.SITE_CONFIG.get("TURNSTILE_SITE_KEY"),
|
||||
env.SITE_CONFIG.get("TURNSTILE_SECRET_KEY"),
|
||||
])
|
||||
|
||||
return {
|
||||
enabled: enabled === "true",
|
||||
siteKey: siteKey || "",
|
||||
secretKey: secretKey || "",
|
||||
}
|
||||
}
|
||||
|
||||
export interface TurnstileVerificationResult {
|
||||
success: boolean
|
||||
reason?: "missing-token" | "verification-failed"
|
||||
}
|
||||
|
||||
export async function verifyTurnstileToken(token?: string | null): Promise<TurnstileVerificationResult> {
|
||||
const config = await getTurnstileConfig()
|
||||
|
||||
if (!config.enabled || !config.siteKey || !config.secretKey) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const trimmedToken = token?.trim()
|
||||
if (!trimmedToken) {
|
||||
return { success: false, reason: "missing-token" }
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `secret=${encodeURIComponent(config.secretKey)}&response=${encodeURIComponent(trimmedToken)}`,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, reason: "verification-failed" }
|
||||
}
|
||||
|
||||
const data = await response.json() as { success: boolean }
|
||||
|
||||
if (!data.success) {
|
||||
return { success: false, reason: "verification-failed" }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error("Turnstile verification error:", error)
|
||||
return { success: false, reason: "verification-failed" }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ export const authSchema = z.object({
|
||||
.regex(/^[a-zA-Z0-9_-]+$/, "用户名只能包含字母、数字、下划线和横杠")
|
||||
.refine(val => !val.includes('@'), "用户名不能是邮箱格式"),
|
||||
password: z.string()
|
||||
.min(8, "密码长度必须大于等于8位")
|
||||
.min(8, "密码长度必须大于等于8位"),
|
||||
turnstileToken: z.string().optional()
|
||||
})
|
||||
|
||||
export type AuthSchema = z.infer<typeof authSchema>
|
||||
export type AuthSchema = z.infer<typeof authSchema>
|
||||
|
||||
Reference in New Issue
Block a user