Files
NightSpire/web-next/lib/transfer-character.ts
T
Inna d95fa932f9 web-next: /transfer-character (transferir personaje por SumUp con todas las condiciones)
- Nueva ruta /transfer-character (10€ SumUp); borra /transfer (Stripe) y actualiza
  el link del panel.
- lib/transfer-character.ts: precheck con todas las condiciones — personaje offline,
  contraseña, token de seguridad, 2FA activo, cuenta destino válida (≠ propia) y,
  para Caballeros de la Muerte, la cuenta destino debe tener un personaje nivel >=55
  y no tener ya un DK; + bloqueo de 5s tras transferencia reciente.
- El hook precheck de PaidServiceConfig ahora recibe {accountId,email,character,body}.
- TransferForm: contraseña + token (con ojos), provider sumup, confirm.

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

82 lines
3.6 KiB
TypeScript

import type { RowDataPacket } from 'mysql2'
import { db, DB } from './db'
import { authenticate } from './auth'
import { checkSecurityToken } from './security-token'
import { is2faEnabled } from './two-factor'
/**
* Condiciones para transferir un personaje a otra cuenta (/transfer-character):
* personaje desconectado, contraseña correcta, token de seguridad, 2FA activo,
* cuenta destino válida, y —para Caballeros de la Muerte— la cuenta destino debe
* tener un personaje de nivel 55+ y no tener ya un DK. Bloqueo de 5 s tras una
* transferencia reciente. Se comprueba ANTES del pago (SumUp).
*/
export async function checkTransferEligibility(
accountId: number,
email: string,
character: string,
body: Record<string, unknown>,
): Promise<{ ok: boolean; message?: string }> {
const destination = String(body.destination_account ?? '').trim()
const password = String(body.password ?? '')
const token = String(body.security_token ?? '').trim()
if (!destination || !password || !token) {
return { ok: false, message: 'Completa la cuenta destino, la contraseña y el token de seguridad.' }
}
// Personaje: de la cuenta, desconectado, clase.
const [chRows] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT class, online FROM characters WHERE name = ? AND account = ? LIMIT 1',
[character, accountId],
)
const ch = chRows[0]
if (!ch) return { ok: false, message: 'El personaje no pertenece a tu cuenta.' }
if (ch.online === 1) return { ok: false, message: 'El personaje debe estar desconectado.' }
const isDK = Number(ch.class) === 6
// Contraseña + token de seguridad + 2FA activo.
if (!(await authenticate(email, password))) return { ok: false, message: 'La contraseña de tu cuenta es incorrecta.' }
if (!(await checkSecurityToken(accountId, token))) return { ok: false, message: 'El token de seguridad no es válido.' }
if (!(await is2faEnabled(accountId))) return { ok: false, message: 'Debes tener el 2FA activado en tu cuenta.' }
// Cuenta destino (por nombre de cuenta).
let destId = 0
try {
const [acc] = await db(DB.auth).query<RowDataPacket[]>('SELECT id FROM account WHERE username = ? LIMIT 1', [destination])
destId = acc[0] ? Number(acc[0].id) : 0
} catch {
return { ok: false, message: 'No se pudo verificar la cuenta de destino.' }
}
if (!destId) return { ok: false, message: 'La cuenta de destino no existe.' }
if (destId === accountId) return { ok: false, message: 'La cuenta de destino no puede ser la tuya.' }
// Condiciones de Caballero de la Muerte.
if (isDK) {
const [lvl] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT 1 FROM characters WHERE account = ? AND level >= 55 LIMIT 1',
[destId],
)
if (!lvl[0]) {
return { ok: false, message: 'La cuenta destino debe tener un personaje de nivel 55 o superior para recibir un Caballero de la Muerte.' }
}
const [dk] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT 1 FROM characters WHERE account = ? AND class = 6 LIMIT 1',
[destId],
)
if (dk[0]) return { ok: false, message: 'La cuenta destino ya tiene un Caballero de la Muerte.' }
}
// Bloqueo de 5 s tras una transferencia reciente de esta cuenta.
try {
const [recent] = await db(DB.default).query<RowDataPacket[]>(
"SELECT id FROM home_stripelog WHERE account_id = ? AND service = 'transfer' AND fulfilled = 1 AND timestamp > (NOW() - INTERVAL 5 SECOND) LIMIT 1",
[accountId],
)
if (recent[0]) return { ok: false, message: 'Espera unos segundos antes de transferir otro personaje.' }
} catch {
/* ignore */
}
return { ok: true }
}