f332fc39ca
- lib/mail.ts: transporte SMTP (nodemailer, mismas creds Gmail que Django). - lib/register.ts: registerAccount (valida, comprueba email existente, crea fila en home_accountactivation reutilizando la tabla de Django, envía email de activación); activateAccount (crea battlenet_accounts con SRP6 v2 + account con SRP6 Grunt, como activate_account_view: expansion=2, battlenet_index=1; borra la activación). - Route handlers /api/auth/register y /api/auth/activate. - Páginas app/[locale]/register (RegisterForm cliente) y app/[locale]/activate-account (ActivateClient auto-POST al abrir el enlace). Textos en messages (Register, Activate). Validado: validaciones (missingFields/passwordMismatch/invalidEmail/passwordTooLong); ciclo completo activación->crea bnet+account->login OK con la cuenta creada (needsSelection=false), password mala->invalidCredentials. Todo en TS, crypto validada. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Link } from '@/i18n/navigation'
|
|
|
|
const ERROR_KEYS = ['invalidLink', 'expiredLink'] as const
|
|
|
|
export function ActivateClient({ hash }: { hash: string }) {
|
|
const t = useTranslations('Activate')
|
|
const [state, setState] = useState<'loading' | 'ok' | 'error'>('loading')
|
|
const [errorKey, setErrorKey] = useState<string>('invalidLink')
|
|
const ran = useRef(false)
|
|
|
|
useEffect(() => {
|
|
if (ran.current) return // evita doble POST en StrictMode
|
|
ran.current = true
|
|
if (!hash) {
|
|
setErrorKey('invalidLink')
|
|
setState('error')
|
|
return
|
|
}
|
|
fetch('/api/auth/activate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ hash }),
|
|
})
|
|
.then((r) => r.json())
|
|
.then((d: { success?: boolean; error?: string }) => {
|
|
if (d.success) setState('ok')
|
|
else {
|
|
setErrorKey((ERROR_KEYS as readonly string[]).includes(d.error ?? '') ? d.error! : 'invalidLink')
|
|
setState('error')
|
|
}
|
|
})
|
|
.catch(() => setState('error'))
|
|
}, [hash])
|
|
|
|
return (
|
|
<div className="text-center">
|
|
{state === 'loading' && <p>{t('activating')}</p>}
|
|
{state === 'ok' && (
|
|
<>
|
|
<p className="text-green-400">{t('success')}</p>
|
|
<p className="mt-4">
|
|
<Link href="/login" className="text-sky-400 underline">
|
|
{t('goLogin')}
|
|
</Link>
|
|
</p>
|
|
</>
|
|
)}
|
|
{state === 'error' && <p className="text-red-400">{t(errorKey)}</p>}
|
|
</div>
|
|
)
|
|
}
|