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." } }