From e00a439dd9a8bd6dbcf7e6a07892b0fb15f833a1 Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 22:46:42 +0000 Subject: [PATCH] Cierra el flujo de login: logout, selector de cuenta y /account con guardas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Route handlers: /api/auth/logout (destroy sesión), /api/auth/select-account (fija la cuenta de juego elegida, valida que pertenece a la bnet en sesión). - app/[locale]/select-account: página SSR (guarda: sin bnetId -> login) + lista de cuentas de juego (SelectAccountForm cliente) -> /account. - app/[locale]/account: página protegida (sin bnetId -> login; sin username -> select-account) con datos de sesión + LogoutButton. - Textos en messages (Account, SelectAccount). Verificado: guardas locale-aware (ES /account -> /login, EN /en/account -> /en/login, 307), logout OK, select-account sin sesión 401. Redirect i18n con next-intl. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../app/[locale]/account/LogoutButton.tsx | 28 ++++++++++ web-next/app/[locale]/account/page.tsx | 33 +++++++++++ .../select-account/SelectAccountForm.tsx | 56 +++++++++++++++++++ web-next/app/[locale]/select-account/page.tsx | 42 ++++++++++++++ web-next/app/api/auth/logout/route.ts | 7 +++ web-next/app/api/auth/select-account/route.ts | 26 +++++++++ web-next/messages/en.json | 14 +++++ web-next/messages/es.json | 14 +++++ 8 files changed, 220 insertions(+) create mode 100644 web-next/app/[locale]/account/LogoutButton.tsx create mode 100644 web-next/app/[locale]/account/page.tsx create mode 100644 web-next/app/[locale]/select-account/SelectAccountForm.tsx create mode 100644 web-next/app/[locale]/select-account/page.tsx create mode 100644 web-next/app/api/auth/logout/route.ts create mode 100644 web-next/app/api/auth/select-account/route.ts diff --git a/web-next/app/[locale]/account/LogoutButton.tsx b/web-next/app/[locale]/account/LogoutButton.tsx new file mode 100644 index 0000000..8b36c09 --- /dev/null +++ b/web-next/app/[locale]/account/LogoutButton.tsx @@ -0,0 +1,28 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import { useRouter } from '@/i18n/navigation' + +export function LogoutButton() { + const t = useTranslations('Account') + const router = useRouter() + const [busy, setBusy] = useState(false) + + async function logout() { + setBusy(true) + await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }) + router.push('/') + router.refresh() + } + + return ( + + ) +} diff --git a/web-next/app/[locale]/account/page.tsx b/web-next/app/[locale]/account/page.tsx new file mode 100644 index 0000000..83d7986 --- /dev/null +++ b/web-next/app/[locale]/account/page.tsx @@ -0,0 +1,33 @@ +import { getTranslations, setRequestLocale } from 'next-intl/server' +import { redirect } from '@/i18n/navigation' +import { getSession } from '@/lib/session' +import { LogoutButton } from './LogoutButton' + +export const dynamic = 'force-dynamic' + +export default async function AccountPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params + setRequestLocale(locale) + const t = await getTranslations('Account') + + const session = await getSession() + if (!session.bnetId) redirect({ href: '/login', locale }) + if (!session.username) redirect({ href: '/select-account', locale }) + + return ( +
+

{t('title')}

+
+

+ {t('bnetAccount')}: {session.bnetEmail} +

+

+ {t('gameAccount')}: {session.username} +

+
+
+ +
+
+ ) +} diff --git a/web-next/app/[locale]/select-account/SelectAccountForm.tsx b/web-next/app/[locale]/select-account/SelectAccountForm.tsx new file mode 100644 index 0000000..f29a5a7 --- /dev/null +++ b/web-next/app/[locale]/select-account/SelectAccountForm.tsx @@ -0,0 +1,56 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import { useRouter } from '@/i18n/navigation' + +interface GameAccount { + id: number + username: string +} + +export function SelectAccountForm({ accounts }: { accounts: GameAccount[] }) { + const t = useTranslations('SelectAccount') + const router = useRouter() + const [busy, setBusy] = useState(null) + const [error, setError] = useState(null) + + async function select(id: number) { + if (busy !== null) return + setBusy(id) + setError(null) + try { + const res = await fetch('/api/auth/select-account', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ accountId: id }), + }) + const data: { success?: boolean } = await res.json() + if (data.success) router.push('/account') + else { + setError(t('error')) + setBusy(null) + } + } catch { + setError(t('error')) + setBusy(null) + } + } + + return ( +
+ {accounts.map((a) => ( + + ))} + {error &&

{error}

} +
+ ) +} diff --git a/web-next/app/[locale]/select-account/page.tsx b/web-next/app/[locale]/select-account/page.tsx new file mode 100644 index 0000000..4dc0d3b --- /dev/null +++ b/web-next/app/[locale]/select-account/page.tsx @@ -0,0 +1,42 @@ +import { getTranslations, setRequestLocale } from 'next-intl/server' +import { redirect } from '@/i18n/navigation' +import { getSession } from '@/lib/session' +import { getGameAccounts } from '@/lib/auth' +import { SelectAccountForm } from './SelectAccountForm' + +export const dynamic = 'force-dynamic' + +export default async function SelectAccountPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params + setRequestLocale(locale) + const t = await getTranslations('SelectAccount') + + const session = await getSession() + if (!session.bnetId) redirect({ href: '/login', locale }) + + const games = await getGameAccounts(session.bnetId!) + // Autoseleccionar si solo hay una + if (games.length === 1) { + // La selección real la hace el cliente/route; aquí mostramos el formulario igual, + // pero con una sola opción es un clic. (La auto-selección en login ya cubre el caso común.) + } + + return ( +
+

{t('title')}

+ {session.bnetEmail && ( +

+ {t('loggedInAs')} {session.bnetEmail} +

+ )} + {games.length > 0 ? ( + <> +

{t('choose')}

+ + + ) : ( +

{t('noAccounts')}

+ )} +
+ ) +} diff --git a/web-next/app/api/auth/logout/route.ts b/web-next/app/api/auth/logout/route.ts new file mode 100644 index 0000000..5d978e3 --- /dev/null +++ b/web-next/app/api/auth/logout/route.ts @@ -0,0 +1,7 @@ +import { getSession } from '@/lib/session' + +export async function POST() { + const session = await getSession() + session.destroy() + return Response.json({ success: true }) +} diff --git a/web-next/app/api/auth/select-account/route.ts b/web-next/app/api/auth/select-account/route.ts new file mode 100644 index 0000000..d0312df --- /dev/null +++ b/web-next/app/api/auth/select-account/route.ts @@ -0,0 +1,26 @@ +import { getGameAccounts } from '@/lib/auth' +import { getSession } from '@/lib/session' +import { setGameAccountSession } from '@/lib/account-session' + +export async function POST(request: Request) { + const session = await getSession() + if (!session.bnetId) { + return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 }) + } + let accountId: number + try { + const body = await request.json() + accountId = Number(body.accountId) + } catch { + return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) + } + + const games = await getGameAccounts(session.bnetId) + const chosen = games.find((g) => g.id === accountId) + if (!chosen) { + return Response.json({ success: false, error: 'invalidAccount' }) + } + setGameAccountSession(session, chosen) + await session.save() + return Response.json({ success: true }) +} diff --git a/web-next/messages/en.json b/web-next/messages/en.json index 91c2205..fb3c522 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -30,5 +30,19 @@ "forgot": "I forgot my password/username", "newHere": "New to the community? You can create an account", "createAccount": "here" + }, + "Account": { + "title": "My account", + "bnetAccount": "Account (Battle.net)", + "gameAccount": "Game account", + "logout": "Log out" + }, + "SelectAccount": { + "title": "Select your game account", + "loggedInAs": "Logged in as", + "choose": "Your Battle.net account has several game accounts. Choose which one to continue with:", + "noAccounts": "No game accounts were found linked to this Battle.net account.", + "entering": "Entering…", + "error": "The account could not be selected." } } diff --git a/web-next/messages/es.json b/web-next/messages/es.json index fd054a7..b31fd24 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -30,5 +30,19 @@ "forgot": "He olvidado mi contraseña/usuario", "newHere": "¿Nuevo en la comunidad? Puedes crear una cuenta", "createAccount": "aquí" + }, + "Account": { + "title": "Mi cuenta", + "bnetAccount": "Cuenta (Battle.net)", + "gameAccount": "Cuenta de juego", + "logout": "Cerrar sesión" + }, + "SelectAccount": { + "title": "Selecciona tu cuenta de juego", + "loggedInAs": "Conectado como", + "choose": "Tu cuenta Battle.net tiene varias cuentas de juego. Elige con cuál quieres continuar:", + "noAccounts": "No se han encontrado cuentas de juego asociadas a esta cuenta Battle.net.", + "entering": "Entrando…", + "error": "No se pudo seleccionar la cuenta." } }