sql >> Base de Datos >  >> RDS >> Mysql

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 y 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 (carpeta de cuentas de usuario), cree un archivo y llámelo config.php. Este archivo se usará 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 comenzamos 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 los archivos incluidos.

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

registro.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="http://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 archivo config.php que creamos anteriormente porque necesitaremos usar la constante INCLUDE_PATH que proporciona config.php dentro de nuestro archivo signup.php. Con esta constante INCLUDE_PATH, también incluimos navbar.php, footer.php y userSignup.php, que contiene la lógica para registrar a 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 haga clic en el botón de carga de imagen, activaremos mediante programación el campo de entrada de archivo usando JQuery y esto mostrará los archivos de la computadora del usuario para que explore su computadora y elija 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 ver en el navegador todavía. Primero demos a este archivo lo que le debemos. Por ahora, dentro de la carpeta assets/css, creemos el archivo style.css que vinculamos en la sección principal.

estilo.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 signup.php son los archivos navbar.php y footer.php. Cree estos dos archivos dentro de la carpeta includes/layouts :

barra de navegación.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>

pie de página.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 JQuery llamado display_profile_image.js y hace exactamente lo que dice su nombre. Cree este archivo dentro de la carpeta assets/js y pegue este código dentro:

mostrar_perfil_imagen.js:

$(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 procesarlos y guardarlos en la base de datos. Cree userSignup.php dentro de la carpeta include/logic y pegue este código dentro:

registro de usuario.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 usamos dos métodos que se derivan de él, a saber:validateUser() y loginById(), que crearemos en breve.

Cree este archivo common_functions.php en su carpeta include/logic :

funciones_comunes.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 queramos 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 adecuada.

Estas dos funciones toman 3 parámetros, a saber, la consulta SQL, los tipos de variables (por ejemplo, 's' significa cadena, 'si' 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 tabla de usuarios donde el nombre de usuario es 'Juan' y tiene 24 años, simplemente 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) e 'i' significa un número entero (edad 20 es un número entero). Esta función nos facilita enormemente el trabajo ya que si queremos realizar una consulta a la base de datos en cien lugares diferentes de nuestra aplicación, no tendremos que hacerlo únicamente en estas dos líneas. Cada una de las funciones en sí tiene alrededor de 8 a 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. Así que es el lugar perfecto para definir estos métodos. Abra 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 usando declaraciones preparadas y esto es importante por razones de seguridad.

Ahora volvamos a nuestro archivo common_functions.php de nuevo. Este archivo contiene 4 funciones importantes que luego serán utilizadas por muchos otros archivos.

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

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

Entonces puede 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.

tabla de roles:

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

tabla de usuarios:

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
)

La tabla de usuarios está relacionada con la tabla de roles en una relación de muchos a uno. Cuando se elimina un rol de la tabla de roles, queremos que todos los usuarios que anteriormente tenían ese role_id como su atributo tengan su valor establecido 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 relación y finalmente completando este formulario de esta manera:

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

índice.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 abra su navegador, vaya a http://localhost/user-accounts/signup.php, complete el formulario con alguna información de prueba (y haga bien en recordarla ya que usaremos el 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 redirigirá a la página de Inicio.

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

En el directorio include/layouts, cree un archivo llamado message.php:

mensajes.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/cierre de sesión del usuario y comenzaremos a trabajar en la sección de administración. Esto parece demasiado trabajo, pero créanme, 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 estés viniendo. Si tiene alguna idea, déjela en los comentarios a continuación. Si encontró algún error o no entendió algo, hágamelo saber en la sección de comentarios para que pueda intentar ayudarlo.

Nos vemos en la siguiente parte.