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:
@@ -56,9 +56,11 @@ export default async function AccountPage({ params }: { params: Promise<{ locale
|
||||
<legend>{t('basicData')}</legend>
|
||||
<div className="separate account-info">
|
||||
<p><i className="fa-solid fa-user account-ficon third-brown"></i> {t('accountName')}:{' '}
|
||||
{games.length > 1
|
||||
? <GameAccountSwitcher accounts={games} currentId={session.accountId ?? 0} />
|
||||
: <span>{wowAccountName(data.gameAccount)}</span>}
|
||||
<GameAccountSwitcher
|
||||
accounts={games}
|
||||
currentId={session.accountId ?? 0}
|
||||
currentLabel={wowAccountName(data.gameAccount)}
|
||||
/>
|
||||
</p>
|
||||
<p><i className="fa-solid fa-at account-ficon third-brown"></i> {t('regMail')}: <span>{maskEmail(data.regMail)}</span></p>
|
||||
<p><i className="fa-solid fa-at account-ficon third-brown"></i> {t('currentMail')}: <span>{maskEmail(data.email)}</span></p>
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -359,7 +359,7 @@ textarea:focus {
|
||||
color: #b1997f;
|
||||
}
|
||||
|
||||
/* Selector de cuenta inline (dentro de «Nombre de la cuenta»). */
|
||||
/* Selector de cuenta inline (dentro de «Nombre de la cuenta») + botón «+». */
|
||||
.account-select {
|
||||
width: auto;
|
||||
min-width: 92px;
|
||||
@@ -369,6 +369,29 @@ textarea:focus {
|
||||
font-size: 17px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.add-account-btn {
|
||||
height: auto;
|
||||
padding: 1px 9px;
|
||||
margin: 0 0 0 6px;
|
||||
font-size: 17px;
|
||||
line-height: 1.4;
|
||||
vertical-align: middle;
|
||||
border: 2px solid #352e2b;
|
||||
background: hsla(0, 0%, 100%, 0.05);
|
||||
color: #ebdec2;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.add-account-btn:hover {
|
||||
background: hsla(0, 0%, 100%, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
.add-account-form {
|
||||
display: inline;
|
||||
}
|
||||
.add-account-input {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Colores de clase de WoW (nombre de personaje) */
|
||||
.class-warrior { color: #c79c6e; }
|
||||
|
||||
Reference in New Issue
Block a user