Gestión de cuentas de usuario, roles, permisos, autenticación PHP y MySQL – Parte 2

Esta es la segunda parte de una serie sobre el sistema de administración de cuentas de usuario, autenticación, roles, permisos. Puedes encontrar la primera parte aquí.

Configuración de la base de datos

Cree una base de datos MySQL llamada cuentas de usuario. Luego, en la carpeta raíz de su proyecto (cuentas de usuario carpeta), cree un archivo y llámelo config.php. Este archivo se utilizará para configurar las variables de la base de datos y luego conectar nuestra aplicación a la base de datos MySQL que acabamos de crear.

config.php:

<?php
	session_start(); // start session
	// connect to database
	$conn = new mysqli("localhost", "root", "", "user-accounts");
	// Check connection
	if ($conn->connect_error) {
	    die("Connection failed: " . $conn->connect_error);
	}
  // define global constants
	define ('ROOT_PATH', realpath(dirname(__FILE__))); // path to the root folder
	define ('INCLUDE_PATH', realpath(dirname(__FILE__) . '/includes' )); // Path to includes folder
	define('BASE_URL', 'http://localhost/user-accounts/'); // the home url of the website
?>

También hemos iniciado la sesión porque necesitaremos usarla más adelante para almacenar la información del usuario que inició sesión, como el nombre de usuario. Al final del archivo, estamos definiendo constantes que nos ayudarán a manejar mejor las inclusiones de archivos.

Nuestra aplicación ahora está conectada a la base de datos MySQL. Creemos un formulario que le permita al usuario ingresar sus datos y registrar su cuenta. Cree un archivo signup.php en la carpeta raíz del proyecto:

signup.php:

<?php include('config.php'); ?>
<?php include(INCLUDE_PATH . '/logic/userSignup.php'); ?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>UserAccounts - Sign up</title>
  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
  <!-- Custom styles -->
  <link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
  <?php include(INCLUDE_PATH . "/layouts/navbar.php") ?>

  <div class="container">
    <div class="row">
      <div class="col-md-4 col-md-offset-4">
        <form class="form" action="signup.php" method="post" enctype="multipart/form-data">
          <h2 class="text-center">Sign up</h2>
          <hr>
          <div class="form-group">
            <label class="control-label">Username</label>
            <input type="text" name="username" class="form-control">
          </div>
          <div class="form-group">
            <label class="control-label">Email Address</label>
            <input type="email" name="email" class="form-control">
          </div>
          <div class="form-group">
            <label class="control-label">Password</label>
            <input type="password" name="password" class="form-control">
          </div>
          <div class="form-group">
            <label class="control-label">Password confirmation</label>
            <input type="password" name="passwordConf" class="form-control">
          </div>
          <div class="form-group" style="text-align: center;">
            <img src="https://via.placeholder.com/150x150" id="profile_img" style="height: 100px; border-radius: 50%" alt="">
            <!-- hidden file input to trigger with JQuery  -->
            <input type="file" name="profile_picture" id="profile_input" value="" style="display: none;">
          </div>
          <div class="form-group">
            <button type="submit" name="signup_btn" class="btn btn-success btn-block">Sign up</button>
          </div>
          <p>Aready have an account? <a href="login.php">Sign in</a></p>
        </form>
      </div>
    </div>
  </div>
<?php include(INCLUDE_PATH . "/layouts/footer.php") ?>
<script type="text/javascript" src="assets/js/display_profile_image.js"></script>

En la primera línea de este archivo, incluimos el config.php archivo que creamos anteriormente porque necesitaremos usar el INCLUDE_PATH constante que config.php proporciona dentro de nuestro archivo signup.php. Usando esta constante INCLUDE_PATH, también incluimos navbar.php, footer.php y userSignup.php que contiene la lógica para registrar un usuario en una base de datos. Crearemos estos archivos muy pronto.

Cerca del final del archivo, hay un campo redondo donde el usuario puede hacer clic para cargar una imagen de perfil. Cuando el usuario hace clic en esta área y selecciona una imagen de perfil de su computadora, primero se muestra una vista previa de esta imagen.

Esta vista previa de la imagen se logra con jquery. Cuando el usuario hace clic en el botón de carga de imagen, activaremos programáticamente el campo de entrada de archivo usando JQuery y esto muestra los archivos de computadora del usuario para que naveguen en su computadora y elijan su imagen de perfil. Cuando seleccionan la imagen, usamos Jquery todavía para mostrar la imagen temporalmente. El código que hace esto se encuentra en nuestro archivo display_profile_image.php que crearemos pronto.

No vea el navegador todavía. Primero demos a este archivo lo que le debemos. Por ahora, por dentro activos /css carpeta, creemos el archivo style.css que vinculamos en la sección de encabezado.

style.css:

@import url('https://fonts.googleapis.com/css?family=Lora');
* { font-family: 'Lora', serif; font-size: 1.04em; }
span.help-block { font-size: .7em; }
form label { font-weight: normal; }
.success_msg { color: '#218823'; }
.form { border-radius: 5px; border: 1px solid #d1d1d1; padding: 0px 10px 0px 10px; margin-bottom: 50px; }
#image_display { height: 90px; width: 80px; float: right; margin-right: 10px; }

En la primera línea de este archivo, estamos importando una fuente de Google llamada ‘Lora’ para que nuestra aplicación tenga una fuente más hermosa.

El siguiente archivo que necesitamos en este signup.php son los archivos navbar.php y footer.php. Crea estos dos archivos dentro incluye / diseños carpeta:

navbar.php:

<div class="container"> <!-- The closing container div is found in the footer -->
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand" href="#">UserAccounts</a>
      </div>
      <ul class="nav navbar-nav navbar-right">
          <li><a href="<?php echo BASE_URL . 'signup.php' ?>"><span class="glyphicon glyphicon-user"></span> Sign Up</a></li>
          <li><a href="<?php echo BASE_URL . 'login.php' ?>"><span class="glyphicon glyphicon-log-in"></span> Login</a></li>
      </ul>
    </div>
  </nav>

footer.php:

    <!-- JQuery -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <!-- Bootstrap JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
  </div> <!-- closing container div -->
</body>
</html>

La última línea del archivo signup.php se vincula a un script de JQuery llamado display_profile_image.js y hace exactamente lo que dice su nombre. Crea este archivo dentro activos / js carpeta y pegue este código dentro de ella:

$(document).ready(function(){
  // when user clicks on the upload profile image button ...
  $(document).on('click', '#profile_img', function(){
    // ...use Jquery to click on the hidden file input field
    $('#profile_input').click();
    // a 'change' event occurs when user selects image from the system.
    // when that happens, grab the image and display it
    $(document).on('change', '#profile_input', function(){
      // grab the file
      var file = $('#profile_input')[0].files[0];
      if (file) {
          var reader = new FileReader();
          reader.onload = function (e) {
              // set the value of the input for profile picture
              $('#profile_input').attr('value', file.name);
              // display the image
              $('#profile_img').attr('src', e.target.result);
          };
          reader.readAsDataURL(file);
      }
    });
  });
});

Y por último, el archivo userSignup.php. Este archivo es donde se envían los datos del formulario de registro para su procesamiento y almacenamiento en la base de datos. Crea userSignup.php dentro incluye / lógica carpeta y pegue este código dentro de ella:

userSignup.php:

<?php include(INCLUDE_PATH . "/logic/common_functions.php"); ?>
<?php
// variable declaration
$username = "";
$email  = "";
$errors  = [];
// SIGN UP USER
if (isset($_POST['signup_btn'])) {
	// validate form values
	$errors = validateUser($_POST, ['signup_btn']);

	// receive all input values from the form. No need to escape... bind_param takes care of escaping
	$username = $_POST['username'];
	$email = $_POST['email'];
	$password = password_hash($_POST['password'], PASSWORD_DEFAULT); //encrypt the password before saving in the database
	$profile_picture = uploadProfilePicture();
	$created_at = date('Y-m-d H:i:s');

	// if no errors, proceed with signup
	if (count($errors) === 0) {
		// insert user into database
		$query = "INSERT INTO users SET username=?, email=?, password=?, profile_picture=?, created_at=?";
		$stmt = $conn->prepare($query);
		$stmt->bind_param('sssss', $username, $email, $password, $profile_picture, $created_at);
		$result = $stmt->execute();
		if ($result) {
		  $user_id = $stmt->insert_id;
			$stmt->close();
			loginById($user_id); // log user in
		 } else {
			 $_SESSION['error_msg'] = "Database error: Could not register user";
		}
	 }
}

Guardé este archivo para el final porque tenía más trabajo. Lo primero es que estamos incluyendo otro archivo llamado common_functions.php en la parte superior de este archivo. Incluimos este archivo porque estamos usando dos métodos que provienen de él, a saber: validateUser () y loginById () que crearemos en breve.

Cree este archivo common_functions.php en su incluir / lógico carpeta:

common_functions.php:

<?php
  // Accept a user ID and returns true if user is admin and false if otherwise
  function isAdmin($user_id) {
    global $conn;
    $sql = "SELECT * FROM users WHERE id=? AND role_id IS NOT NULL LIMIT 1";
    $user = getSingleRecord($sql, 'i', [$user_id]); // get single user from database
    if (!empty($user)) {
      return true;
    } else {
      return false;
    }
  }
  function loginById($user_id) {
    global $conn;
    $sql = "SELECT u.id, u.role_id, u.username, r.name as role FROM users u LEFT JOIN roles r ON u.role_id=r.id WHERE u.id=? LIMIT 1";
    $user = getSingleRecord($sql, 'i', [$user_id]);

    if (!empty($user)) {
      // put logged in user into session array
      $_SESSION['user'] = $user;
      $_SESSION['success_msg'] = "You are now logged in";
      // if user is admin, redirect to dashboard, otherwise to homepage
      if (isAdmin($user_id)) {
        $permissionsSql = "SELECT p.name as permission_name FROM permissions as p
                            JOIN permission_role as pr ON p.id=pr.permission_id
                            WHERE pr.role_id=?";
        $userPermissions = getMultipleRecords($permissionsSql, "i", [$user['role_id']]);
        $_SESSION['userPermissions'] = $userPermissions;
        header('location: ' . BASE_URL . 'admin/dashboard.php');
      } else {
        header('location: ' . BASE_URL . 'index.php');
      }
      exit(0);
    }
  }

// Accept a user object, validates user and return an array with the error messages
  function validateUser($user, $ignoreFields) {
  		global $conn;
      $errors = [];
      // password confirmation
      if (isset($user['passwordConf']) && ($user['password'] !== $user['passwordConf'])) {
        $errors['passwordConf'] = "The two passwords do not match";
      }
      // if passwordOld was sent, then verify old password
      if (isset($user['passwordOld']) && isset($user['user_id'])) {
        $sql = "SELECT * FROM users WHERE id=? LIMIT 1";
        $oldUser = getSingleRecord($sql, 'i', [$user['user_id']]);
        $prevPasswordHash = $oldUser['password'];
        if (!password_verify($user['passwordOld'], $prevPasswordHash)) {
          $errors['passwordOld'] = "The old password does not match";
        }
      }
      // the email should be unique for each user for cases where we are saving admin user or signing up new user
      if (in_array('save_user', $ignoreFields) || in_array('signup_btn', $ignoreFields)) {
        $sql = "SELECT * FROM users WHERE email=? OR username=? LIMIT 1";
        $oldUser = getSingleRecord($sql, 'ss', [$user['email'], $user['username']]);
        if (!empty($oldUser['email']) && $oldUser['email'] === $user['email']) { // if user exists
          $errors['email'] = "Email already exists";
        }
        if (!empty($oldUser['username']) && $oldUser['username'] === $user['username']) { // if user exists
          $errors['username'] = "Username already exists";
        }
      }

      // required validation
  	  foreach ($user as $key => $value) {
        if (in_array($key, $ignoreFields)) {
          continue;
        }
  			if (empty($user[$key])) {
  				$errors[$key] = "This field is required";
  			}
  	  }
  		return $errors;
  }
  // upload's user profile profile picture and returns the name of the file
  function uploadProfilePicture()
  {
    // if file was sent from signup form ...
    if (!empty($_FILES) && !empty($_FILES['profile_picture']['name'])) {
        // Get image name
        $profile_picture = date("Y.m.d") . $_FILES['profile_picture']['name'];
        // define Where image will be stored
        $target = ROOT_PATH . "/assets/images/" . $profile_picture;
        // upload image to folder
        if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $target)) {
          return $profile_picture;
          exit();
        }else{
          echo "Failed to upload image";
        }
    }
  }

Permítanme llamar su atención sobre 2 funciones importantes en este archivo. Ellos son: getSingleRecord () y getMultipleRecords(). Estas funciones son muy importantes porque en cualquier parte de nuestra aplicación, cuando queremos seleccionar un registro de la base de datos, simplemente llamaremos a la función getSingleRecord () y le pasaremos la consulta SQL. Si queremos seleccionar varios registros, lo adivinó, simplemente llamaremos a la función getMultipleRecords () también pasando la consulta SQL apropiada.

Estas dos funciones toman 3 parámetros, a saber, la consulta SQL, los tipos de variables (por ejemplo, ‘S‘significa cuerda’y‘que significa cadena y entero, etc.) y, por último, un tercer parámetro que es una matriz de todos los valores que la consulta necesita para ejecutarse.

Por ejemplo, si quiero seleccionar de la usuarios tabla donde el nombre de usuario es ‘John’ y la edad de 24, solo escribiré mi consulta de esta manera:

$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query

$user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query

En la llamada a la función, ‘S‘representa el tipo de cadena (ya que el nombre de usuario’ John ‘es una cadena) y’y‘significa un número entero (la edad de 20 es un número entero). Esta función facilita enormemente nuestro trabajo porque si queremos realizar una consulta de base de datos en un centenar de lugares diferentes de nuestra aplicación, no tendremos que hacerlo solo en estas dos líneas. Cada una de las funciones tiene entre 8 y 10 líneas de código, por lo que no tenemos que repetir el código. Implementemos estos métodos a la vez.

El archivo config.php se incluirá en cada archivo donde se realicen consultas a la base de datos, ya que contiene la configuración de la base de datos. Por tanto, es el lugar perfecto para definir estos métodos. Abierto config.php una vez más y simplemente agregue estos métodos al final del archivo:

config.php:

// ...More code here ...

function getMultipleRecords($sql, $types = null, $params = []) {
  global $conn;
  $stmt = $conn->prepare($sql);
  if (!empty($params) && !empty($params)) { // parameters must exist before you call bind_param() method
    $stmt->bind_param($types, ...$params);
  }
  $stmt->execute();
  $result = $stmt->get_result();
  $user = $result->fetch_all(MYSQLI_ASSOC);
  $stmt->close();
  return $user;
}
function getSingleRecord($sql, $types, $params) {
  global $conn;
  $stmt = $conn->prepare($sql);
  $stmt->bind_param($types, ...$params);
  $stmt->execute();
  $result = $stmt->get_result();
  $user = $result->fetch_assoc();
  $stmt->close();
  return $user;
}
function modifyRecord($sql, $types, $params) {
  global $conn;
  $stmt = $conn->prepare($sql);
  $stmt->bind_param($types, ...$params);
  $result = $stmt->execute();
  $stmt->close();
  return $result;
}

Estamos utilizando declaraciones preparadas y esto es importante por razones de seguridad.

Ahora de vuelta a nosotros common_functions.php archivo de nuevo. Este archivo contiene 4 funciones importantes que serán utilizadas más adelante por muchos otros archivos.

Cuando el usuario se registra, queremos asegurarnos de que proporcionó los datos correctos, por lo que llamamos al validateUser () función, que proporciona este archivo. Si se seleccionó una imagen de perfil, la cargamos llamando al uploadProfilePicture() función, que proporciona este archivo.

Si guardamos con éxito al usuario en la base de datos, queremos iniciar sesión de inmediato, por lo que llamamos al loginById() función, que proporciona este archivo. Cuando un usuario inicia sesión, queremos saber si es administrador o normal, por lo que llamamos al isAdmin () función, que proporciona este archivo. Si encontramos que son administradores (si isAdmin () devuelve verdadero), los redirigimos al tablero. Si son usuarios normales, redirigimos a la página de inicio.

Para que pueda ver que nuestro archivo common_functions.php es muy importante. Usaremos todas estas funciones cuando estemos trabajando en nuestra sección de administración, lo que reduce en gran medida nuestro trabajo y evita la repetición de código.

Para permitir que el usuario se registre, creemos la tabla de usuarios. Pero dado que la tabla de usuarios está relacionada con la tabla de roles, primero crearemos la tabla de roles.

roles chaquete:

CREATE TABLE `roles` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) NOT NULL,
 `description` text NOT NULL,
  PRIMARY KEY (`id`)
)

usuarios chaquete:

CREATE TABLE `users`(
    `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
    `role_id` INT(11) DEFAULT NULL,
    `username` VARCHAR(255) UNIQUE NOT NULL,
    `email` VARCHAR(255) UNIQUE NOT NULL,
    `password` VARCHAR(255) NOT NULL,
    `profile_picture` VARCHAR(255) DEFAULT NULL,
    `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
    CONSTRAINT `users_ibfk_1` FOREIGN KEY(`role_id`) REFERENCES `roles`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION
)

El usuarios La tabla está relacionada con la roles tabla en una relación de varios a uno. Cuando se elimina un rol de la tabla de roles, queremos que todos los usuarios tengan ese Identificación del rol como atributo para que su valor se establezca en NULL. Esto significa que el usuario ya no será administrador.

Si está creando la tabla manualmente, haga bien en agregar esta restricción. Si está utilizando PHPMyAdmin, puede hacerlo haciendo clic en la pestaña de estructura en la tabla de usuarios, luego en la tabla de vista de relaciones y finalmente llenando este formulario como este:

En este punto, nuestro sistema permite que un usuario se registre y luego, después de registrarse, se registran automáticamente. Pero después de iniciar sesión, como se muestra en la loginById () función, son redirigidos a la página de inicio (index.php). Creemos esa página. En la raíz de la aplicación, cree un archivo llamado index.php.

index.php:

<?php include("config.php") ?>
<?php include(INCLUDE_PATH . "/logic/common_functions.php"); ?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>UserAccounts - Home</title>
  <!-- Bootstrap CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
  <!-- Custome styles -->
  <link rel="stylesheet" href="static/css/style.css">
</head>
<body>
    <?php include(INCLUDE_PATH . "/layouts/navbar.php") ?>
    <?php include(INCLUDE_PATH . "/layouts/messages.php") ?>
    <h1>Home page</h1>
    <?php include(INCLUDE_PATH . "/layouts/footer.php") ?>

Ahora abre tu navegador, ve a http: //localhost/user-accounts/signup.php, complete el formulario con información de prueba (y haga bien en recordarla, ya que usaremos al usuario más adelante para iniciar sesión), luego haga clic en el botón de registro. Si todo salió bien, el usuario se guardará en la base de datos y nuestra aplicación lo redireccionará a la página de inicio.

En la página de inicio, verá un error que surge porque estamos incluyendo el archivo messages.php que aún no hemos creado. Creámoslo de una vez.

En el incluye / diseños directorio, crea un archivo llamado messages.php:

messages.php:

<?php if (isset($_SESSION['success_msg'])): ?>
  <div class="alert <?php echo 'alert-success'; ?> alert-dismissible" role="alert">
    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <?php
      echo $_SESSION['success_msg'];
      unset($_SESSION['success_msg']);
    ?>
  </div>
<?php endif; ?>

<?php if (isset($_SESSION['error_msg'])): ?>
  <div class="alert alert-danger alert-dismissible" role="alert">
    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <?php
      echo $_SESSION['error_msg'];
      unset($_SESSION['error_msg']);
    ?>
  </div>
<?php endif; ?>

Ahora actualice la página de inicio y el error desaparecerá.

Y eso es todo por esta parte. En la siguiente parte, continuaremos con la validación del formulario de registro, el inicio de sesión / cierre de sesión del usuario y comenzaremos a trabajar en la sección de administración. Esto parece demasiado trabajo, pero créame, es sencillo, especialmente porque ya hemos escrito un código que facilita nuestro trabajo en la sección de administración.

Gracias por seguir. Espero que vengas. Si tiene alguna idea, déjela en los comentarios a continuación. Si encontró algún error o no entendió algo, avíseme en la sección de comentarios para que pueda intentar ayudarlo.

Nos vemos en la siguiente parte.

Awa Melvine

4.6/5 - (644 votos)

Deja un comentario