fix(auth): parsear error_code del fragmento de GoTrue en /auth/confirm
GoTrue redirige con #error=...&error_code=... cuando el token de invitación/recovery ya fue usado o venció, en vez de emitir PASSWORD_RECOVERY. Antes esto se descubría recién a los 8s (timeout genérico); ahora se lee el fragmento al montar y se muestra un mensaje específico (otp_expired, access_denied, etc.) al instante. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,26 @@ import { useRouter } from 'next/navigation'
|
|||||||
|
|
||||||
type Stage = 'validating' | 'ready' | 'submitting' | 'error'
|
type Stage = 'validating' | 'ready' | 'submitting' | 'error'
|
||||||
|
|
||||||
|
const DEFAULT_LINK_ERROR = 'Pedile al administrador que te envíe una nueva invitación.'
|
||||||
|
|
||||||
|
// GoTrue redirige con #error=...&error_code=...&error_description=... cuando
|
||||||
|
// el token ya fue usado, venció, o es inválido — en vez de emitir PASSWORD_RECOVERY.
|
||||||
|
// Mapeo a mensajes accionables; default cubre códigos no contemplados acá.
|
||||||
|
function describeLinkError(errorCode: string | null): string {
|
||||||
|
switch (errorCode) {
|
||||||
|
case 'otp_expired':
|
||||||
|
return 'Este link ya fue usado o venció. ' + DEFAULT_LINK_ERROR
|
||||||
|
case 'access_denied':
|
||||||
|
return 'El acceso fue denegado para este link. ' + DEFAULT_LINK_ERROR
|
||||||
|
default:
|
||||||
|
return 'El link de invitación no es válido. ' + DEFAULT_LINK_ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ConfirmPage() {
|
export default function ConfirmPage() {
|
||||||
const [stage, setStage] = useState<Stage>('validating')
|
const [stage, setStage] = useState<Stage>('validating')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [linkErrorMessage, setLinkErrorMessage] = useState(DEFAULT_LINK_ERROR)
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -18,6 +35,20 @@ export default function ConfirmPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// GoTrue ya nos dice en el fragmento si el token es inválido/vencido/usado
|
||||||
|
// (#error=...&error_code=...) — leerlo evita esperar el timeout genérico.
|
||||||
|
const hash = window.location.hash.startsWith('#')
|
||||||
|
? window.location.hash.slice(1)
|
||||||
|
: window.location.hash
|
||||||
|
const hashParams = new URLSearchParams(hash)
|
||||||
|
const errorCode = hashParams.get('error_code')
|
||||||
|
|
||||||
|
if (errorCode) {
|
||||||
|
setLinkErrorMessage(describeLinkError(errorCode))
|
||||||
|
setStage('error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Supabase emite PASSWORD_RECOVERY tanto para recovery como para invite
|
// Supabase emite PASSWORD_RECOVERY tanto para recovery como para invite
|
||||||
// cuando el token de la URL (#access_token) es válido.
|
// cuando el token de la URL (#access_token) es válido.
|
||||||
const { data: listener } = supabase.auth.onAuthStateChange((event) => {
|
const { data: listener } = supabase.auth.onAuthStateChange((event) => {
|
||||||
@@ -30,7 +61,8 @@ export default function ConfirmPage() {
|
|||||||
setStage((s) => (s === 'validating' && session ? 'ready' : s))
|
setStage((s) => (s === 'validating' && session ? 'ready' : s))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sin evento ni sesión tras unos segundos = token inválido/vencido/ausente.
|
// Sin evento ni sesión tras unos segundos = token inválido/vencido/ausente
|
||||||
|
// sin que GoTrue haya mandado un error explícito en el fragmento.
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setStage((s) => (s === 'validating' ? 'error' : s))
|
setStage((s) => (s === 'validating' ? 'error' : s))
|
||||||
}, 8000)
|
}, 8000)
|
||||||
@@ -87,7 +119,7 @@ export default function ConfirmPage() {
|
|||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
<h1 className="login-title">Link inválido o vencido</h1>
|
<h1 className="login-title">Link inválido o vencido</h1>
|
||||||
<p role="alert" className="field-error login-error">
|
<p role="alert" className="field-error login-error">
|
||||||
Pedile al administrador que te envíe una nueva invitación.
|
{linkErrorMessage}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user