Files
NightSpire/web-next/app/api/character/quest/route.ts
T
Inna eaae16e33a web-next: change-race/faction/level-up/gold por SumUp + rastreador de misiones
- /change-race-character (3€), /change-faction-character (5€, valida condiciones:
  sin hermandad/correo/subastas/capitán-arena y oro <= cap por nivel),
  /level-up-character (10€), /gold-character (oro por correo SOAP). Cada uno borra
  su ruta antigua y actualiza el link del panel.
- SumUp: hook precheck en PaidServiceConfig (condiciones cambio de facción);
  columna home_stripelog.metadata para campos extra (gold_amount).
- /quest-character: rastreador de estado de misiones/cadenas (clase, Hijos de
  Hodir, misión por ID) en personaje offline; cooldowns por categoría, Turnstile,
  IDs de misión verificados en wotlkdb 3.3.5a. Tabla home_questsearchhistory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:39:28 +00:00

80 lines
3.2 KiB
TypeScript

import type { RowDataPacket } from 'mysql2'
import { getSession } from '@/lib/session'
import { db, DB } from '@/lib/db'
import { getClassCss } from '@/lib/character-info'
import { verifyTurnstile } from '@/lib/turnstile'
import { QUEST_OPTIONS, questStatuses, cooldownRemainingMs, recordSearch, type QuestCategory } from '@/lib/quest-tracker'
export async function POST(request: Request) {
const session = await getSession()
if (!session.accountId || !session.username) {
return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 })
}
let body: Record<string, unknown> = {}
try {
body = await request.json()
} catch {
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
}
const character = String(body.character ?? '').trim()
const optionKey = String(body.option ?? '').trim()
if (!character || !optionKey) return Response.json({ success: false, error: 'missingFields' })
const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || undefined
if (!(await verifyTurnstile(String(body.turnstile ?? ''), ip))) {
return Response.json({ success: false, error: 'captcha' })
}
// Personaje: debe ser de la cuenta y estar desconectado.
let guid = 0
let level = 0
let classCss = ''
try {
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT guid, level, class, online FROM characters WHERE name = ? AND account = ? LIMIT 1',
[character, session.accountId],
)
const c = rows[0]
if (!c) return Response.json({ success: false, error: 'characterNotOwned' })
if (c.online === 1) return Response.json({ success: false, error: 'characterOnline' })
guid = Number(c.guid)
level = Number(c.level)
classCss = getClassCss(Number(c.class))
} catch {
return Response.json({ success: false, error: 'genericError' })
}
// Resuelve categoría + misiones a comprobar.
let category: QuestCategory
let quests: number[]
let label: string
if (optionKey === 'specific') {
const qid = Number(body.questId)
if (!Number.isInteger(qid) || qid <= 0) return Response.json({ success: false, error: 'invalidQuestId' })
if (level < 10) return Response.json({ success: false, error: 'levelTooLow' })
category = 'specific'
quests = [qid]
label = `Misión #${qid}`
} else {
const opt = QUEST_OPTIONS.find((o) => o.key === optionKey)
if (!opt) return Response.json({ success: false, error: 'invalidOption' })
if (opt.classCss && opt.classCss !== classCss) return Response.json({ success: false, error: 'wrongClass' })
if (level < opt.minLevel) return Response.json({ success: false, error: 'levelTooLow' })
if (opt.quests.length === 0) return Response.json({ success: false, error: 'noData' })
category = opt.category
quests = opt.quests
label = opt.label
}
const remaining = await cooldownRemainingMs(character, category)
if (remaining > 0) {
return Response.json({ success: false, error: 'cooldown', minutes: Math.ceil(remaining / 60000) })
}
const statuses = await questStatuses(guid, quests)
await recordSearch(character, category)
return Response.json({ success: true, label, statuses })
}