Tutorial: Sistema de Invitaciones con HTML, PHP y MySQL
Un sistema de invitaciones es útil para controlar el acceso a una plataforma, fomentar el crecimiento orgánico o gestionar programas de referencia. El proceso general implica que un usuario registrado genere una invitación, esta se envíe por correo electrónico al invitado con un token único, y el invitado utilice este token para registrarse.
Para este tutorial, asumimos que tienes un entorno de desarrollo con PHP y MySQL funcionando (por ejemplo, XAMPP, WAMP o un servidor web con Apache/Nginx, PHP y MySQL/MariaDB).
1. Configuración de la Base de Datos
Primero, necesitamos una base de datos y dos tablas: una para almacenar a los usuarios registrados y otra para las invitaciones enviadas.
a. Crear la Base de Datos
Puedes usar phpMyAdmin
o la línea de comandos de MySQL:
CREATE DATABASE sistema_invitaciones_db;
USE sistema_invitaciones_db;
b. Crear la Tabla usuarios
Esta tabla almacenará a los usuarios que ya están registrados o que se registrarán a través de una invitación.
CREATE TABLE usuarios (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, -- Se recomienda almacenar hashes de contraseñas
rol VARCHAR(50) DEFAULT 'invitado', -- 'admin', 'usuario', 'invitado'
fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
c. Crear la Tabla invitaciones
Esta tabla guardará las invitaciones enviadas, su estado y el token único.
CREATE TABLE invitaciones (
id INT AUTO_INCREMENT PRIMARY KEY,
email_invitado VARCHAR(100) NOT NULL UNIQUE,
token VARCHAR(255) NOT NULL UNIQUE, -- Token único para cada invitación
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
fecha_expiracion DATETIME, -- Opcional: para que las invitaciones caduquen
estado ENUM('pendiente', 'aceptada', 'rechazada', 'expirada') DEFAULT 'pendiente',
id_usuario_emisor INT, -- Quién envió la invitación
FOREIGN KEY (id_usuario_emisor) REFERENCES usuarios(id) ON DELETE SET NULL
);
Nota: La columna
password
en la tablausuarios
debe almacenar la contraseña hasheada, no el texto plano.
2. Archivo de Conexión a la Base de Datos (db_config.php
)
Crearemos un archivo para manejar la conexión a la base de datos.
<?php
// db_config.php
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // Usar 'root' sin contraseña es común en desarrollo, ¡NO EN PRODUCCIÓN!
define('DB_PASS', '');
define('DB_NAME', 'sistema_invitaciones_db');
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
die("Error de conexión a la base de datos: " . $conn->connect_error);
}
$conn->set_charset("utf8");
?>
Seguridad: En un entorno de producción, crea un usuario MySQL con privilegios mínimos necesarios y una contraseña fuerte.
3. Envío de Correos Electrónicos (PHP Mailer)
PHP tiene la función mail()
, pero es muy básica y poco confiable para enviar correos electrónicos en producción. Se recomienda usar una librería como PHPMailer.
a. Instalar PHPMailer
Descarga PHPMailer o instálalo vía Composer:
composer require phpmailer/phpmailer
Si no usas Composer, descarga el ZIP de GitHub (src
en tu proyecto.
b. Archivo de Utilidad de Correo (mail_util.php
)
Crearemos una función para enviar correos usando PHPMailer.
<?php
// mail_util.php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require 'vendor/autoload.php'; // Si usas Composer
// Si no usas Composer, descomenta y ajusta las siguientes líneas:
// require 'PHPMailer-master/src/Exception.php';
// require 'PHPMailer-master/src/PHPMailer.php';
// require 'PHPMailer-master/src/SMTP.php';
function enviarInvitacionEmail($destinatarioEmail, $token) {
$mail = new PHPMailer(true); // Pasar `true` habilita excepciones
try {
// Configuración del servidor SMTP (ej. Gmail, Mailtrap para pruebas)
$mail->isSMTP();
$mail->Host = 'smtp.gmail.com'; // O tu servidor SMTP
$mail->SMTPAuth = true;
$mail->Username = 'tu_email@gmail.com'; // Tu dirección de correo
$mail->Password = 'tu_contraseña_de_aplicacion'; // Tu contraseña de aplicación o SMTP
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // Usar SMTPS
$mail->Port = 465;
// Remitente y destinatario
$mail->setFrom('tu_email@gmail.com', 'Tu Nombre de Empresa');
$mail->addAddress($destinatarioEmail);
// Contenido del correo
$mail->isHTML(true);
$mail->Subject = '¡Has sido invitado a unirte a nuestra plataforma!';
$enlaceInvitacion = 'http://localhost/sistema_invitaciones/registro.php?token=' . $token; // Asegúrate de que esta URL sea correcta
$mail->Body = 'Hola,<br><br>Has sido invitado a unirte a nuestra plataforma. Haz clic en el siguiente enlace para registrarte:<br><br>' .
'<a href="' . $enlaceInvitacion . '">' . $enlaceInvitacion . '</a><br><br>' .
'Este enlace es válido por un tiempo limitado (si configuraste expiración).<br><br>' .
'Saludos,<br>El equipo de [Tu Empresa]';
$mail->AltBody = 'Hola, Has sido invitado a unirte a nuestra plataforma. Copia y pega el siguiente enlace en tu navegador: ' . $enlaceInvitacion;
$mail->send();
return true;
} catch (Exception $e) {
// En un entorno de producción, registra el error en un log, no lo muestres al usuario.
error_log("Error al enviar invitación a {$destinatarioEmail}: {$mail->ErrorInfo}");
return false;
}
}
?>
Importante para Gmail: Si usas Gmail como servidor SMTP, necesitarás generar una contraseña de aplicación en la configuración de seguridad de tu cuenta de Google, ya que la contraseña de tu cuenta normal no funcionará para la autenticación SMTP.
4. Archivo para Enviar Invitaciones (enviar_invitacion.php
)
Este archivo tendrá un formulario para que un usuario (asumiremos que está "logueado" o simularemos su ID para pruebas) envíe invitaciones.
<?php
// enviar_invitacion.php
session_start(); // Inicia la sesión para simular un usuario logueado
include 'db_config.php';
include 'mail_util.php'; // Incluye la función para enviar correos
// Simulación de usuario logueado para pruebas
// En una aplicación real, el id_usuario_emisor vendría de la sesión del usuario actual.
$_SESSION['id_usuario'] = 1; // Asume que el usuario con ID 1 es el que envía la invitación.
// Asegúrate de que este ID exista en tu tabla `usuarios`.
// Para fines de este tutorial, puedes insertar un usuario manualmente si no tienes un sistema de login.
$mensaje = '';
if (!isset($_SESSION['id_usuario'])) {
// Si no hay un usuario logueado, redirige o muestra un error.
header("Location: login.php"); // O a una página de error
exit();
}
$id_usuario_emisor = $_SESSION['id_usuario'];
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$email_invitado = $conn->real_escape_string($_POST['email_invitado']);
// 1. Generar un token único
$token = bin2hex(random_bytes(32)); // Genera un token aleatorio de 64 caracteres hexadecimales
// 2. Insertar la invitación en la base de datos
$stmt = $conn->prepare("INSERT INTO invitaciones (email_invitado, token, id_usuario_emisor, fecha_expiracion) VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))");
// Asignamos una fecha de expiración de 7 días, opcional
$stmt->bind_param("ssi", $email_invitado, $token, $id_usuario_emisor);
if ($stmt->execute()) {
// 3. Enviar el correo electrónico de invitación
if (enviarInvitacionEmail($email_invitado, $token)) {
$mensaje = "<div style='color: green;'>¡Invitación enviada exitosamente a " . htmlspecialchars($email_invitado) . "!</div>";
} else {
$mensaje = "<div style='color: orange;'>Invitación guardada, pero no se pudo enviar el correo a " . htmlspecialchars($email_invitado) . ". Verifica la configuración de SMTP.</div>";
}
} else {
// Si el correo ya existe en invitaciones (UNIQUE), se disparará un error.
if ($conn->errno == 1062) { // 1062 es el código de error para entrada duplicada (UNIQUE constraint)
$mensaje = "<div style='color: red;'>Error: Ya existe una invitación pendiente para " . htmlspecialchars($email_invitado) . ".</div>";
} else {
$mensaje = "<div style='color: red;'>Error al guardar la invitación: " . $stmt->error . "</div>";
}
}
$stmt->close();
}
$conn->close();
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enviar Invitación</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; }
.container { max-width: 500px; margin: auto; background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="email"] { width: calc(100% - 22px); padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background-color: #0056b3; }
.message { margin-top: 20px; text-align: center; padding: 10px; border: 1px solid; border-radius: 4px; }
a { display: block; text-align: center; margin-top: 20px; color: #007bff; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h2>Enviar Invitación</h2>
<?php echo $mensaje; ?>
<form action="enviar_invitacion.php" method="POST">
<div class="form-group">
<label for="email_invitado">Email del Invitado:</label>
<input type="email" id="email_invitado" name="email_invitado" required>
</div>
<button type="submit">Enviar Invitación</button>
</form>
<a href="index.php">Ver estado de las invitaciones</a>
</div>
</body>
</html>
Nota: Para que este archivo funcione, necesitas tener un usuario con
id=1
en tu tablausuarios
o ajustar el valor de$_SESSION['id_usuario']
para que coincida con un ID existente.
5. Archivo para el Registro de Usuarios a través de Invitación (registro.php
)
Este archivo procesará el token de invitación y permitirá al invitado crear una cuenta.
<?php
// registro.php
include 'db_config.php';
$mensaje = '';
$token_valido = false;
$email_invitado = '';
if (isset($_GET['token']) && !empty($_GET['token'])) {
$token = $conn->real_escape_string($_GET['token']);
// 1. Verificar si el token es válido y no ha sido usado o expirado
$stmt = $conn->prepare("SELECT email_invitado FROM invitaciones WHERE token = ? AND estado = 'pendiente' AND (fecha_expiracion IS NULL OR fecha_expiracion > NOW())");
$stmt->bind_param("s", $token);
$stmt->execute();
$resultado = $stmt->get_result();
if ($resultado->num_rows > 0) {
$fila = $resultado->fetch_assoc();
$email_invitado = $fila['email_invitado'];
$token_valido = true;
} else {
$mensaje = "<div style='color: red;'>El enlace de invitación no es válido, ha expirado o ya ha sido utilizado.</div>";
}
$stmt->close();
} else if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Proceso de registro cuando se envía el formulario
$token = $conn->real_escape_string($_POST['token']);
$nombre = $conn->real_escape_string($_POST['nombre']);
$email = $conn->real_escape_string($_POST['email']);
$password = $_POST['password']; // Contraseña en texto plano
$confirm_password = $_POST['confirm_password'];
// Validaciones básicas
if (empty($nombre) || empty($email) || empty($password) || empty($confirm_password)) {
$mensaje = "<div style='color: red;'>Todos los campos son obligatorios.</div>";
} elseif ($password !== $confirm_password) {
$mensaje = "<div style='color: red;'>Las contraseñas no coinciden.</div>";
} else {
// Volver a verificar el token antes de registrar
$stmt_check = $conn->prepare("SELECT id FROM invitaciones WHERE token = ? AND email_invitado = ? AND estado = 'pendiente' AND (fecha_expiracion IS NULL OR fecha_expiracion > NOW())");
$stmt_check->bind_param("ss", $token, $email);
$stmt_check->execute();
$resultado_check = $stmt_check->get_result();
if ($resultado_check->num_rows > 0) {
// Hashear la contraseña
$password_hashed = password_hash($password, PASSWORD_DEFAULT);
// Insertar el nuevo usuario
$conn->begin_transaction(); // Iniciar transacción
try {
$stmt_user = $conn->prepare("INSERT INTO usuarios (nombre, email, password) VALUES (?, ?, ?)");
$stmt_user->bind_param("sss", $nombre, $email, $password_hashed);
if ($stmt_user->execute()) {
// Actualizar el estado de la invitación a 'aceptada'
$stmt_update = $conn->prepare("UPDATE invitaciones SET estado = 'aceptada' WHERE token = ?");
$stmt_update->bind_param("s", $token);
$stmt_update->execute();
$conn->commit(); // Confirmar la transacción
$mensaje = "<div style='color: green;'>¡Registro exitoso! Ya puedes iniciar sesión.</div>";
$token_valido = false; // Deshabilitar el formulario de registro
} else {
$conn->rollback(); // Revertir la transacción
if ($conn->errno == 1062) { // Correo duplicado
$mensaje = "<div style='color: red;'>Error: El correo electrónico ya está registrado.</div>";
} else {
$mensaje = "<div style='color: red;'>Error al registrar usuario: " . $stmt_user->error . "</div>";
}
}
$stmt_user->close();
$stmt_update->close();
} catch (Exception $e) {
$conn->rollback(); // Revertir la transacción en caso de excepción
$mensaje = "<div style='color: red;'>Error en la transacción: " . $e->getMessage() . "</div>";
}
} else {
$mensaje = "<div style='color: red;'>El enlace de invitación no es válido o ya ha sido utilizado.</div>";
}
$stmt_check->close();
}
} else {
$mensaje = "<div style='color: red;'>Acceso inválido. No se proporcionó un token de invitación.</div>";
}
$conn->close();
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registro de Usuario</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; }
.container { max-width: 500px; margin: auto; background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="email"], input[type="password"] { width: calc(100% - 22px); padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
button { background-color: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background-color: #218838; }
.message { margin-top: 20px; text-align: center; padding: 10px; border: 1px solid; border-radius: 4px; }
a { display: block; text-align: center; margin-top: 20px; color: #007bff; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h2>Registro de Usuario</h2>
<?php echo $mensaje; ?>
<?php if ($token_valido): ?>
<form action="registro.php" method="POST">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
<div class="form-group">
<label for="nombre">Nombre:</label>
<input type="text" id="nombre" name="nombre" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" value="<?php echo htmlspecialchars($email_invitado); ?>" readonly>
</div>
<div class="form-group">
<label for="password">Contraseña:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirmar Contraseña:</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit">Registrarse</button>
</form>
<?php endif; ?>
<a href="login.php">¿Ya tienes una cuenta? Iniciar Sesión</a>
</div>
</body>
</html>
6. Archivo para Listar Invitaciones (index.php
)
Este archivo (opcional) mostrará el estado de las invitaciones enviadas.
<?php
// index.php (Lista de invitaciones enviadas)
session_start();
include 'db_config.php';
// Simulación de usuario logueado para pruebas
$_SESSION['id_usuario'] = 1;
if (!isset($_SESSION['id_usuario'])) {
header("Location: login.php"); // Redirigir a login si no hay sesión
exit();
}
$id_usuario_emisor = $_SESSION['id_usuario'];
$invitaciones = [];
$stmt = $conn->prepare("SELECT email_invitado, token, fecha_creacion, fecha_expiracion, estado FROM invitaciones WHERE id_usuario_emisor = ? ORDER BY fecha_creacion DESC");
$stmt->bind_param("i", $id_usuario_emisor);
$stmt->execute();
$resultado = $stmt->get_result();
while ($fila = $resultado->fetch_assoc()) {
$invitaciones[] = $fila;
}
$stmt->close();
$conn->close();
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Estado de Invitaciones</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; }
.container { max-width: 800px; margin: auto; background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #333; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
th { background-color: #f2f2f2; color: #333; }
.status-pendiente { color: orange; font-weight: bold; }
.status-aceptada { color: green; font-weight: bold; }
.status-expirada { color: gray; font-weight: bold; }
.status-rechazada { color: red; font-weight: bold; }
.add-button { display: inline-block; background-color: #007bff; color: white; padding: 10px 15px; border-radius: 4px; text-decoration: none; margin-bottom: 20px; }
.add-button:hover { background-color: #0056b3; }
.no-invitations { text-align: center; color: #666; padding: 20px; border: 1px dashed #ccc; border-radius: 5px; }
</style>
</head>
<body>
<div class="container">
<h2>Estado de Invitaciones Enviadas</h2>
<a href="enviar_invitacion.php" class="add-button">Enviar Nueva Invitación</a>
<?php if (!empty($invitaciones)): ?>
<table>
<thead>
<tr>
<th>Email Invitado</th>
<th>Token (Solo para depuración)</th>
<th>Fecha Creación</th>
<th>Fecha Expiración</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
<?php foreach ($invitaciones as $inv): ?>
<tr>
<td><?php echo htmlspecialchars($inv['email_invitado']); ?></td>
<td><?php echo htmlspecialchars($inv['token']); ?></td>
<td><?php echo htmlspecialchars($inv['fecha_creacion']); ?></td>
<td><?php echo htmlspecialchars($inv['fecha_expiracion'] ? $inv['fecha_expiracion'] : 'N/A'); ?></td>
<td class="status-<?php echo htmlspecialchars($inv['estado']); ?>">
<?php echo htmlspecialchars(ucfirst($inv['estado'])); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="no-invitations">
<p>Aún no has enviado ninguna invitación.</p>
</div>
<?php endif; ?>
</div>
</body>
</html>
7. Prueba del Sistema
- Configura MySQL: Asegúrate de que MySQL esté funcionando y crea la base de datos
sistema_invitaciones_db
y las tablasusuarios
einvitaciones
como se indicó en el Paso 1. - Crea un usuario para pruebas: Si no tienes un sistema de login, inserta un usuario manualmente en la tabla
usuarios
para simular que está enviando invitaciones.SQLINSERT INTO usuarios (nombre, email, password, rol) VALUES ('Admin Test', 'admin@example.com', '$2y$10$Q7y0r2R1S0v1N8x9Y0z2U.mX4R5Q6T7U8V9W0X1Y2Z3A4B5C6D7E8F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T3U4V5W6X7Y8Z9A0B1C2D3E4F5G6H7I8J9K0L1M2N3O4P5Q6R7S8', 'admin'); -- La contraseña hasheada aquí es para 'password123', puedes generar la tuya con password_hash('tu_contraseña', PASSWORD_DEFAULT);
- Descarga PHPMailer: Coloca los archivos de PHPMailer en tu proyecto (la carpeta
vendor
si usas Composer, o la carpetaPHPMailer-master/src
si no). - Ajusta
mail_util.php
: Modificatu_email@gmail.com
ytu_contraseña_de_aplicacion
con tus credenciales de SMTP. - Coloca los archivos: Guarda
db_config.php
,mail_util.php
,enviar_invitacion.php
,registro.php
eindex.php
en una carpeta de tu servidor web (ej.,htdocs/sistema_invitaciones/
). - Accede:
- Para enviar invitaciones:
http://localhost/sistema_invitaciones/enviar_invitacion.php
- Para ver el estado:
http://localhost/sistema_invitaciones/index.php
- El enlace de registro se enviará por correo y tendrá el formato:
http://localhost/sistema_invitaciones/registro.php?token=XXXXX
- Para enviar invitaciones:
Consideraciones Adicionales y Mejoras:
- Seguridad de Contraseñas: Siempre usa
password_hash()
ypassword_verify()
para manejar contraseñas. Nunca almacenes contraseñas en texto plano. - Validación y Saneamiento: Este tutorial usa
real_escape_string()
yintval()
, pero en una aplicación real, implementa una validación más robusta (ej.,filter_var()
para correos electrónicos, validación de longitud de cadenas, etc.). - Errores y Depuración: En producción, desactiva la visualización de errores de PHP (
display_errors = Off
) y usa un sistema de logging (error_log()
). - Autenticación de Usuario: Para un sistema real, necesitarías un módulo de inicio de sesión (
login.php
) que establezca la sesión del usuario ($_SESSION['id_usuario']
) de forma segura. - Reenvío de Invitaciones: Podrías añadir una función para reenviar una invitación si el invitado no la recibió o la perdió.
- Eliminación de Invitaciones Expiradas: Considera un script que se ejecute periódicamente (una tarea
cron
) para limpiar las invitaciones expiradas de la base de datos. - Mensajes Flash: Para una mejor experiencia de usuario, implementa mensajes flash que se muestren una vez y luego desaparezcan después de una redirección.
- Interfaz de Usuario (UI): Mejora el diseño CSS y HTML para una experiencia más atractiva.
Este tutorial te proporciona una base sólida para crear un sistema de invitaciones funcional. ¿Hay alguna parte específica que te gustaría expandir o alguna funcionalidad adicional que te interese añadir?
Referencias Bibliográficas:
American Psychological Association.
PHP Manual. (s.f.). MySQLi. Obtenido de
PHP Manual. (s.f.). password_hash. Obtenido de
PHPMailer. (s.f.). PHPMailer: The only code you need to send email!. Obtenido de
Comentarios
Publicar un comentario