Post Top Ad

Your Ad Spot

jueves, 7 de mayo de 2020

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 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 hemos comenzado la sesión porque tendremos que usarla más adelante para almacenar información de usuario registrada como 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. Creemos 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:
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="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 config.php proporciona dentro de nuestro archivo signup.php. Usando esta constante INCLUDE_PATH, también estamos incluyendo 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 hace clic en el botón cargar 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 puedan navegar por su computadora y elegir 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 veas 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 de cabecera.
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 hacer 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. Cree estos dos archivos dentro de la   carpeta incluye / diseños :
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 enlaza con 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 de él:
display_profile_image.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 su procesamiento y almacenamiento en la base de datos. Cree userSignup.php dentro de la  carpeta incluye / logic y pegue este código dentro de él:
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   carpeta include / logic :
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ítame 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 múltiples registros, lo adivinó, simplemente simplemente llamaremos a la función getMultipleRecords () al pasar 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 e 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 deseo seleccionar de la tabla de usuarios donde el nombre de usuario es 'John' y 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) y ' i ' significa entero (la edad de 20 es un entero). Esta función hace que nuestro trabajo sea inmensamente fácil porque si queremos realizar una consulta de base de datos en cientos de lugares diferentes en nuestra aplicación, no tendremos que solo estas dos líneas. Las funciones en sí tienen cada una de 8 a 10 líneas de código, por lo que nos ahorramos repetir el código. Implementemos estos métodos de una 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 lo tanto, es el lugar perfecto para definir estos métodos. Abra config.php una vez más y solo 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 regrese a nuestro archivo common_functions.php nuevamente. 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 correctamente al usuario en la base de datos, queremos iniciar 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 descubrimos que son admin (si isAdmin () devuelve verdadero), los redirigimos al panel de control. Si son usuarios normales, redirigimos a la página de inicio.
Para que pueda ver nuestro archivo common_functions.php es muy importante. Utilizaremos todas estas funciones cuando trabajemos 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 como 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 previamente tengan 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, inician sesión automáticamente. Pero después de iniciar sesión, como se muestra en la función  loginById ()  , son redirigidos 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.
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 abra su navegador, vaya a  http: //localhost/user-accounts/signup.php , complete el formulario con alguna información de prueba (y haga bien en recordarlos ya que usaremos al usuario más adelante para iniciar sesión), luego haga clic El botón de registro. Si todo salió bien, el usuario se guardará en la base de datos y nuestra aplicación redirigirá 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. Vamos a crearlo de una vez.
En el directorio de inclusiones / diseños , cree 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 validando el 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 confía en mí, es sencillo, especialmente porque ya hemos escrito un código que facilita nuestro trabajo en la sección Admin. 
Gracias por seguir. Espero que vengas Si tienes alguna idea, escríbela en los comentarios a continuación. Si encontraste algún error o no entendiste algo, avísame en la sección de comentarios para que pueda intentar ayudarte.
Nos vemos en la siguiente parte .

No hay comentarios.:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

outbrain

Páginas