web-next: botón «+» para crear cuentas de juego (WOW2, WOW3…)

Junto al «Nombre de la cuenta» hay un botón «+» que pide la contraseña y crea
una nueva cuenta de juego bajo la Battle.net. Ruta /api/account/add-game-account:
verifica la contraseña (authenticate contra battlenet_accounts), calcula el
siguiente battlenet_index (máx 8) y crea la fila `account` con SRP6 Grunt
derivado de la contraseña (gameMakeRegistration) — igual que la activación, así
la cuenta sirve para entrar al juego. Recarga completa al crear/cambiar.
GameAccountSwitcher siempre presente (select si >1, texto si 1) + «+».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 15:50:18 +00:00
parent e62ab94054
commit c2e35d33ea
6 changed files with 183 additions and 19 deletions
@@ -0,0 +1,60 @@
import type { ResultSetHeader, RowDataPacket } from 'mysql2'
import { db, DB } from '@/lib/db'
import { getSession } from '@/lib/session'
import { authenticate } from '@/lib/auth'
import { gameMakeRegistration, makeGameAccountUsername, normalizeEmail } from '@/lib/bnet'
const MAX_GAME_ACCOUNTS = 8 // Battle.net: WoW1…WoW8
/**
* Crea una nueva cuenta de juego (WoW2, WoW3…) bajo la Battle.net de la sesión.
* Requiere la contraseña (para derivar el SRP6 de la cuenta y que sirva in-game,
* ya que todas las cuentas de la bnet usan la misma contraseña).
*/
export async function POST(request: Request) {
const session = await getSession()
if (!session.bnetId || !session.bnetEmail) {
return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 })
}
let password = ''
try {
const body = await request.json()
password = String(body.password ?? '')
} catch {
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
}
if (!password) return Response.json({ success: false, error: 'invalidPassword' })
// Verifica la contraseña contra la Battle.net.
const bnet = await authenticate(session.bnetEmail, password)
if (!bnet) return Response.json({ success: false, error: 'invalidPassword' })
const emailNorm = normalizeEmail(session.bnetEmail)
// Siguiente índice de cuenta de juego.
const [rows] = await db(DB.auth).query<RowDataPacket[]>(
'SELECT COALESCE(MAX(battlenet_index), 0) AS maxIdx, COUNT(*) AS n FROM account WHERE battlenet_account = ?',
[session.bnetId],
)
const count = Number(rows[0]?.n ?? 0)
if (count >= MAX_GAME_ACCOUNTS) {
return Response.json({ success: false, error: 'maxAccounts' })
}
const nextIndex = Number(rows[0]?.maxIdx ?? 0) + 1
// Crea la cuenta de juego (SRP6 Grunt con la contraseña de la bnet).
const gameUsername = makeGameAccountUsername(session.bnetId, nextIndex)
const game = gameMakeRegistration(gameUsername, password)
try {
await db(DB.auth).query<ResultSetHeader>(
'INSERT INTO account (username, salt, verifier, email, reg_mail, joindate, last_ip, expansion, battlenet_account, battlenet_index) ' +
'VALUES (?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?)',
[gameUsername, game.salt, game.verifier, emailNorm, emailNorm, '0.0.0.0', 2, session.bnetId, nextIndex],
)
} catch {
return Response.json({ success: false, error: 'createFailed' })
}
return Response.json({ success: true, index: nextIndex })
}