Files
BYW-VoiceHub/server/api/meow/bind.post.ts
T

236 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createError, defineEventHandler, readBody } from 'h3'
import { db } from '~/drizzle/db'
import { users } from '~/drizzle/schema'
import { and, eq, ne } from 'drizzle-orm'
import { getServerTimestamp } from '~~/server/utils/serverTime'
import {
delStore,
delStoreIfValue,
getStore,
hashStateCode,
incrStore,
parseStoreJson,
setStore,
verifyStateCode
} from '~~/server/utils/captchaStore'
import { randomInt } from 'node:crypto'
// 生成6位数字验证码
function generateVerificationCode(): string {
return randomInt(100000, 1000000).toString()
}
export default defineEventHandler(async (event) => {
try {
// 从认证中间件获取用户信息
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '请先登录'
})
}
const userId = user.id
const body = await readBody(event)
const { action, meowId, verificationCode } = body
// 如果是验证并绑定操作
if (action === 'verify_and_bind' && verificationCode) {
const stateKey = `meow-bind:${userId}`
const storedRaw = await getStore(stateKey)
const storedData = parseStoreJson<{
meowId: string
codeHash: string
expiresAt: number
}>(storedRaw)
if (
!storedData ||
typeof storedData.meowId !== 'string' ||
typeof storedData.codeHash !== 'string' ||
!Number.isFinite(storedData.expiresAt)
) {
if (storedRaw) await delStoreIfValue(stateKey, storedRaw)
throw createError({
statusCode: 400,
message: '验证码已过期,请重新发送'
})
}
if (storedData.meowId !== meowId) {
throw createError({
statusCode: 400,
message: '验证码不匹配'
})
}
const now = getServerTimestamp()
if (now > storedData.expiresAt) {
await delStoreIfValue(stateKey, storedRaw!)
throw createError({
statusCode: 400,
message: '验证码已过期,请重新发送'
})
}
const remainingTtl = Math.max(1, Math.ceil((storedData.expiresAt - now) / 1000))
const attemptKey = `${stateKey}:attempts:${storedData.codeHash}`
const attemptCount = await incrStore(attemptKey, remainingTtl)
if (attemptCount > 5) {
await delStoreIfValue(stateKey, storedRaw!)
await delStore(attemptKey)
throw createError({ statusCode: 400, message: '验证码错误次数过多,请重新发送' })
}
if (!verifyStateCode(stateKey, String(verificationCode), storedData.codeHash)) {
if (attemptCount >= 5) {
await delStoreIfValue(stateKey, storedRaw!)
await delStore(attemptKey)
throw createError({ statusCode: 400, message: '验证码错误次数过多,请重新发送' })
}
throw createError({
statusCode: 400,
message: '验证码错误'
})
}
if (!(await delStoreIfValue(stateKey, storedRaw!))) {
throw createError({ statusCode: 400, message: '验证码已使用,请重新发送' })
}
await delStore(attemptKey)
// 验证通过,绑定账号
await db
.update(users)
.set({
meowNickname: meowId,
meowBoundAt: new Date()
})
.where(eq(users.id, userId))
return {
success: true,
message: 'MeoW 账号绑定成功!'
}
}
// 如果是发送验证码操作
if (action === 'send_verification') {
if (!meowId || !meowId.trim()) {
throw createError({
statusCode: 400,
message: '请输入 MeoW 用户 ID'
})
}
// 验证用户ID格式
if (meowId.includes('/')) {
throw createError({
statusCode: 400,
message: '用户 ID 不能包含斜杠'
})
}
// 检查用户是否存在
const userResult = await db.select().from(users).where(eq(users.id, userId)).limit(1)
const userExists = userResult[0]
if (!userExists) {
throw createError({
statusCode: 404,
message: '用户不存在'
})
}
// 检查是否有其他用户绑定了相同的 MeoW ID(仅用于记录,不阻止绑定)
const existingUsers = await db
.select({
name: users.name,
username: users.username
})
.from(users)
.where(and(eq(users.meowNickname, meowId), ne(users.id, userId)))
if (existingUsers.length > 0) {
console.log(
`MeoW ID "${meowId}" 已被 ${existingUsers.length} 个其他用户绑定:`,
existingUsers.map((u) => `${u.name}(${u.username})`).join(', ')
)
}
// 生成验证码
const code = generateVerificationCode()
const expiresAt = getServerTimestamp() + 5 * 60 * 1000 // 5分钟过期
const stateKey = `meow-bind:${userId}`
const storedValue = JSON.stringify({
meowId,
codeHash: hashStateCode(stateKey, code),
expiresAt
})
// 存储验证码
await setStore(stateKey, storedValue, 5 * 60)
// 发送验证码到 MeoW
const message = `VoiceHub 账号绑定验证码:${code}(5分钟内有效)`
const title = 'VoiceHub 账号绑定'
try {
// 使用 GET 请求发送验证码
const meowUrl = `https://api.chuckfang.com/${encodeURIComponent(meowId)}/${encodeURIComponent(title)}/${encodeURIComponent(message)}`
const response = await fetch(meowUrl, {
method: 'GET',
headers: {
'User-Agent': 'VoiceHub/1.0'
}
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const result = await response.json()
if (result.status !== 200) {
throw new Error(result.message || '发送失败')
}
return {
success: true,
message: '验证码已发送到您的 MeoW,请查收并输入验证码完成绑定',
requiresVerification: true
}
} catch (error) {
// 清除验证码
await delStoreIfValue(stateKey, storedValue)
console.error('发送 MeoW 验证码失败:', error)
throw createError({
statusCode: 500,
message: '发送验证码失败,请检查 MeoW ID 是否正确或稍后重试'
})
}
}
// 如果不是有效的操作
throw createError({
statusCode: 400,
message: '无效的操作'
})
} catch (error: any) {
console.error('MeoW 绑定失败:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
message: '绑定失败,请稍后重试'
})
}
})