Ir al contenido

Recuperar contraseña

Restablece tu contraseña cuando no puedes acceder a tu cuenta. Puedes recibir el enlace en tu correo principal o en el de recuperación si lo configuraste.

  1. En la pantalla de login, clic en “¿Olvidaste tu contraseña?”.
  2. Introduce el correo electrónico de tu cuenta.
  3. Elige dónde recibir el enlace:
    • Correo principal — El que usas para iniciar sesión
    • Correo de recuperación — Si configuraste uno alternativo
  4. Clic en “Enviar enlace” — recibirás un email con un enlace.
  5. Abre el email y clic en el enlace — te redirige a la pantalla de cambio.
  6. Introduce tu nueva contraseña (2 veces para confirmar).
  7. Clic en “Cambiar contraseña” — contraseña actualizada.
CampoObligatorioDescripción
Email de cuentaSiEl correo con el que te registraste
Nueva contraseñaSiMínimo 8 caracteres
Confirmar contraseñaSiDebe coincidir con la nueva
ReglaDescripción
Longitud mínima8 caracteres
CoincidenciaAmbos campos deben ser iguales
  • Email recibido con asunto: “Recuperación de contraseña - Factux”
  • Toast de confirmación: “Contraseña actualizada correctamente”
  • Puedes iniciar sesión con la nueva contraseña
MensajeCausaSolución
”No se encontró una cuenta con este correo”Email no registradoVerifica el email o regístrate
”El enlace ha expirado”Token usado o más de 24hSolicita un nuevo enlace
”Las contraseñas no coinciden”Campos diferentesEscribe la misma contraseña en ambos
”Error al enviar el correo”Problema de servidorReintenta en unos minutos

ALERTA: El enlace de recuperación es válido por 24 horas y solo puede usarse una vez.

Detalles técnicos — Recuperar contraseña

Sección titulada «Detalles técnicos — Recuperar contraseña»
Flujo de recuperación completo

Edge Function: send-password-reset-email

Ubicación: /supabase/functions/send-password-reset-email/index.ts

Proceso paso a paso:

// 1. Usuario solicita reset
interface ResetPasswordRequest {
email: string;
sendToRecoveryEmail?: boolean;
}
// 2. Buscar usuario
const { data: users } = await supabaseClient
.rpc('get_user_by_email', { user_email: email.toLowerCase() });
// 3. Obtener recovery_email si se solicita
const { data: profile } = await supabaseClient
.from("profiles")
.select("recovery_email")
.eq("user_id", user.id)
.maybeSingle();
// 4. Determinar email destino
let targetEmail = email.toLowerCase();
if (sendToRecoveryEmail && profile?.recovery_email) {
targetEmail = profile.recovery_email;
}
// 5. Generar token seguro
const token = crypto.randomUUID();
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
// 6. Guardar token en base de datos
await supabaseClient
.from("password_reset_tokens")
.insert({
user_id: user.id,
email: email.toLowerCase(),
token,
expires_at: expiresAt.toISOString(),
});
// 7. Crear URL de reset
const resetUrl = `${APP_URL}/reset-password?token=${token}`;
// 8. Enviar email via SendGrid
await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${SENDGRID_API_KEY}`,
},
body: JSON.stringify({
template_id: "d-b937c4c6573a49c3bd745df56b5cb2e6",
personalizations: [{
to: [{ email: targetEmail }],
dynamic_template_data: {
reset_password_url: resetUrl,
user_email: email.toLowerCase(),
expiration_hours: "24"
}
}]
}),
});

Edge Function: update-password-with-token

Ubicación: /supabase/functions/update-password-with-token/index.ts

Proceso de cambio:

// 1. Validar token
const { data: tokenData } = await supabaseClient
.from("password_reset_tokens")
.select("*")
.eq("token", token)
.is("used_at", null)
.gt("expires_at", new Date().toISOString())
.single();
// 2. Actualizar contraseña
await supabaseClient.auth.admin.updateUserById(
tokenData.user_id,
{ password: newPassword }
);
// 3. Marcar token como usado
await supabaseClient
.from("password_reset_tokens")
.update({ used_at: new Date().toISOString() })
.eq("id", tokenData.id);

Tabla de tokens:

ColumnaTipoDescripción
idUUIDID del token
user_idUUIDUsuario asociado
emailTEXTEmail para referencia
tokenTEXTUUID único del token
expires_atTIMESTAMPExpiración (created_at + 24h)
used_atTIMESTAMPCuándo se usó (NULL si no usado)
created_atTIMESTAMPCuándo se creó

Constraints:

CONSTRAINT valid_expiry CHECK (expires_at > created_at)

Limpieza automática:

-- Función que ejecuta cron para limpiar tokens viejos
CREATE OR REPLACE FUNCTION cleanup_expired_password_tokens()
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM public.password_reset_tokens
WHERE expires_at < NOW()
OR used_at IS NOT NULL AND used_at < NOW() - INTERVAL '7 days';
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Configuración SendGrid:

Variable de entornoValor
SENDGRID_API_KEYAPI key de SendGrid
SENDGRID_FROM_EMAILnoreply@facturik.com
SENDGRID_TEMPLATE_IDd-b937c4c6573a49c3bd745df56b5cb2e6
APP_URLhttps://clientes.facturik.com

Query de diagnóstico:

-- Tokens activos pendientes de uso
SELECT prt.*, u.email as user_email
FROM password_reset_tokens prt
JOIN auth.users u ON prt.user_id = u.id
WHERE prt.expires_at > NOW()
AND prt.used_at IS NULL
ORDER BY prt.created_at DESC;
-- Historial de resets de un usuario
SELECT * FROM password_reset_tokens
WHERE user_id = '<user_id>'
ORDER BY created_at DESC
LIMIT 10;
-- Tokens expirados sin usar (posible problema)
SELECT COUNT(*) FROM password_reset_tokens
WHERE expires_at < NOW()
AND used_at IS NULL;