SISTEMA DE INVITACIONES CON HTML, PHP Y MYSQL


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:

SQL
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.

SQL
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.

SQL
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 tabla usuarios 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
<?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:

Bash
composer require phpmailer/phpmailer

Si no usas Composer, descarga el ZIP de GitHub (https://github.com/PHPMailer/PHPMailer/archive/master.zip) y extrae la carpeta src en tu proyecto.

b. Archivo de Utilidad de Correo (mail_util.php)

Crearemos una función para enviar correos usando PHPMailer.

PHP
<?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
<?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 tabla usuarios 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
<?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
<?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

  1. Configura MySQL: Asegúrate de que MySQL esté funcionando y crea la base de datos sistema_invitaciones_db y las tablas usuarios e invitaciones como se indicó en el Paso 1.
  2. 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.
    SQL
    INSERT 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);
    
  3. Descarga PHPMailer: Coloca los archivos de PHPMailer en tu proyecto (la carpeta vendor si usas Composer, o la carpeta PHPMailer-master/src si no).
  4. Ajusta mail_util.php: Modifica tu_email@gmail.com y tu_contraseña_de_aplicacion con tus credenciales de SMTP.
  5. Coloca los archivos: Guarda db_config.php, mail_util.php, enviar_invitacion.php, registro.php e index.php en una carpeta de tu servidor web (ej., htdocs/sistema_invitaciones/).
  6. 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

Consideraciones Adicionales y Mejoras:

  • Seguridad de Contraseñas: Siempre usa password_hash() y password_verify() para manejar contraseñas. Nunca almacenes contraseñas en texto plano.
  • Validación y Saneamiento: Este tutorial usa real_escape_string() y intval(), 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. (2020). Publication Manual of the American Psychological Association (7th ed.). American Psychological Association.

PHP Manual. (s.f.). MySQLi. Obtenido de https://www.php.net/manual/es/book.mysqli.php

PHP Manual. (s.f.). password_hash. Obtenido de https://www.php.net/manual/es/function.password-hash.php

PHPMailer. (s.f.). PHPMailer: The only code you need to send email!. Obtenido de https://github.com/PHPMailer/PHPMailer

Comentarios