Mysql
 sql >> Base de données >  >> RDS >> Mysql

Gestion des comptes utilisateurs, rôles, permissions, authentification PHP et MySQL - Partie 2

Ceci est la deuxième partie d'une série sur le système de gestion des comptes d'utilisateurs, l'authentification, les rôles, les autorisations. Vous pouvez trouver la première partie ici.

Configuration de la base de données

Créez une base de données MySQL appelée comptes d'utilisateurs. Ensuite, dans le dossier racine de votre projet (dossier des comptes d'utilisateurs), créez un fichier et appelez-le config.php. Ce fichier sera utilisé pour configurer les variables de la base de données puis connecter notre application à la base de données MySQL que nous venons de créer.

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
?>

Nous avons également démarré la session car nous aurons besoin de l'utiliser plus tard pour stocker les informations de l'utilisateur connecté telles que le nom d'utilisateur. À la fin du fichier, nous définissons des constantes qui nous aideront à mieux gérer les inclusions de fichiers.

Notre application est maintenant connectée à la base de données MySQL. Créons un formulaire qui permet à un utilisateur d'entrer ses coordonnées et d'enregistrer son compte. Créez un fichier signup.php dans le dossier racine du projet :

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

Sur la toute première ligne de ce fichier, nous incluons le fichier config.php que nous avons créé précédemment, car nous devrons utiliser la constante INCLUDE_PATH fournie par config.php dans notre fichier signup.php. En utilisant cette constante INCLUDE_PATH, nous incluons également navbar.php, footer.php et userSignup.php qui contient la logique d'enregistrement d'un utilisateur dans une base de données. Nous créerons ces fichiers très prochainement.

Vers la fin du fichier, il y a un champ rond sur lequel l'utilisateur peut cliquer pour télécharger une image de profil. Lorsque l'utilisateur clique sur cette zone et sélectionne une image de profil sur son ordinateur, un aperçu de cette image s'affiche d'abord.

Cet aperçu de l'image est réalisé avec jquery. Lorsque l'utilisateur clique sur le bouton de téléchargement d'image, nous déclenchons par programme le champ de saisie de fichier à l'aide de JQuery, ce qui fait apparaître les fichiers informatiques de l'utilisateur pour qu'il puisse parcourir son ordinateur et choisir son image de profil. Lorsqu'ils sélectionnent l'image, nous utilisons encore Jquery pour afficher temporairement l'image. Le code qui fait cela se trouve dans notre fichier display_profile_image.php que nous créerons bientôt.

Ne pas afficher sur le navigateur pour l'instant. Donnons d'abord à ce dossier ce que nous lui devons. Pour l'instant, dans le dossier assets/css, créons le fichier style.css que nous avons lié dans la section head.

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; }

Sur la première ligne de ce fichier, nous importons une police Google nommée "Lora" pour que notre application ait une police plus belle.

Le fichier suivant dont nous avons besoin dans ce signup.php est les fichiers navbar.php et footer.php. Créez ces deux fichiers dans le dossier includes/layouts :

barre de navigation.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>

pied de page.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 toute dernière ligne du fichier signup.php est liée à un script JQuery nommé display_profile_image.js et il fait exactement ce que son nom indique. Créez ce fichier dans le dossier assets/js et collez ce code à l'intérieur :

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);
      }
    });
  });
});

Et enfin, le fichier userSignup.php. C'est dans ce fichier que les données du formulaire d'inscription sont envoyées pour traitement et enregistrement dans la base de données. Créez userSignup.php dans le dossier includes/logic et collez ce code à l'intérieur :

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";
		}
	 }
}

J'ai enregistré ce fichier pour la fin car il y avait plus de travail. La première chose est que nous incluons encore un autre fichier nommé common_functions.php en haut de ce fichier. Nous incluons ce fichier car nous utilisons deux méthodes qui en découlent, à savoir :validateUser() et loginById() que nous créerons sous peu.

Créez ce fichier common_functions.php dans votre dossier 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";
        }
    }
  }

Permettez-moi d'attirer votre attention sur 2 fonctions importantes dans ce fichier. Ce sont : getSingleRecord() et getMultipleRecords(). Ces fonctions sont très importantes car n'importe où dans l'ensemble de notre application, lorsque nous voulons sélectionner un enregistrement dans la base de données, nous appelons simplement la fonction getSingleRecord() et lui transmettons la requête SQL. Si nous voulons sélectionner plusieurs enregistrements, vous l'aurez deviné, nous appellerons simplement la fonction getMultipleRecords() en passant la requête SQL appropriée.

Ces deux fonctions prennent 3 paramètres à savoir la requête SQL, les types de variables (par exemple, 's' signifie chaîne, 'si' signifie chaîne et entier, etc.) et enfin un troisième paramètre qui est un tableau de toutes les valeurs qui dont la requête a besoin pour s'exécuter.

Par exemple, si je veux sélectionner dans la table des utilisateurs où le nom d'utilisateur est "John" et l'âge de 24 ans, j'écrirai simplement ma requête comme ceci :

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

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

Dans l'appel de fonction, 's' représente le type de chaîne (puisque le nom d'utilisateur 'John' est une chaîne) et 'i' signifie un nombre entier (l'âge de 20 ans est un nombre entier). Cette fonction rend notre travail extrêmement facile car si nous voulons effectuer une requête de base de données à cent endroits différents de notre application, nous n'aurons pas à nous contenter de ces deux lignes. Les fonctions elles-mêmes ont chacune environ 8 à 10 lignes de code, ce qui nous évite de répéter du code. Implémentons ces méthodes immédiatement.

Le fichier config.php sera inclus dans chaque fichier où des requêtes de base de données sont effectuées car il contient la configuration de la base de données. C'est donc l'endroit idéal pour définir ces méthodes. Ouvrez à nouveau config.php et ajoutez simplement ces méthodes à la fin du fichier :

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;
}

Nous utilisons des déclarations préparées et ceci est important pour des raisons de sécurité.

Revenons maintenant à notre fichier common_functions.php. Ce fichier contient 4 fonctions importantes qui seront utilisées plus tard par de nombreux autres fichiers.

Lorsque l'utilisateur s'enregistre, nous voulons nous assurer qu'il a fourni les bonnes données. Nous appelons donc la fonction validateUser() , fournie par ce fichier. Si une image de profil a été sélectionnée, nous la téléchargeons en appelant la fonction uploadProfilePicture() , fournie par ce fichier.

Si nous réussissons à enregistrer l'utilisateur dans la base de données, nous voulons les connecter immédiatement, nous appelons donc la fonction loginById() , fournie par ce fichier. Lorsqu'un utilisateur se connecte, nous voulons savoir s'il est administrateur ou normal, nous appelons donc la fonction isAdmin() , fournie par ce fichier. Si nous constatons qu'ils sont admin (si isAdmin() renvoie true), nous les redirigeons vers le tableau de bord. S'il s'agit d'utilisateurs normaux, nous redirigeons vers la page d'accueil.

Vous pouvez donc voir que notre fichier common_functions.php est très important. Nous utiliserons toutes ces fonctions lorsque nous travaillerons sur notre section admin ce qui réduit considérablement notre travail et évite la répétition de code.

Pour permettre à l'utilisateur de s'inscrire, créons la table des utilisateurs. Mais comme la table des utilisateurs est liée à la table des rôles, nous allons d'abord créer la table des rôles.

tableau des rôles :

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

tableau des utilisateurs :

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 table des utilisateurs est liée à la table des rôles dans une relation plusieurs-à-un. Lorsqu'un rôle est supprimé de la table des rôles, nous souhaitons que tous les utilisateurs ayant précédemment cet identifiant de rôle comme attribut aient sa valeur définie sur NULL. Cela signifie que l'utilisateur ne sera plus administrateur.

Si vous créez la table manuellement, faites bien d'ajouter cette contrainte. Si vous utilisez PHPMyAdmin, vous pouvez le faire en cliquant sur l'onglet structure du tableau des utilisateurs, puis sur le tableau de la vue des relations, puis enfin en remplissant ce formulaire comme ceci :

À ce stade, notre système permet à un utilisateur de s'enregistrer, puis après s'être enregistré, il est automatiquement connecté. Mais après s'être connecté, comme indiqué dans la fonction loginById() , il est redirigé vers la page d'accueil (index.php). Créons cette page. A la racine de l'application, créez un fichier nommé 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") ?>

Ouvrez maintenant votre navigateur, allez sur http://localhost/user-accounts/signup.php, remplissez le formulaire avec quelques informations de test (et faites bien de vous en souvenir car nous utiliserons l'utilisateur plus tard pour vous connecter), puis cliquez sur le bouton d'inscription. Si tout s'est bien passé, l'utilisateur sera enregistré dans la base de données et notre application redirigera vers la page d'accueil.

Sur la page d'accueil, vous verrez une erreur qui survient parce que nous incluons le fichier messages.php que nous n'avons pas encore créé. Créons-le immédiatement.

Dans le répertoire includes/layouts, créez un fichier nommé 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; ?>

Actualisez maintenant la page d'accueil et l'erreur a disparu.

Et c'est tout pour cette partie. Dans la partie suivante, nous continuerons avec la validation du formulaire d'inscription, la connexion/déconnexion de l'utilisateur et commencerons à travailler sur la section admin. Cela semble trop de travail, mais croyez-moi, c'est simple, d'autant plus que nous avons déjà écrit du code qui facilite notre travail dans la section Admin.

Merci de votre soutien. J'espère que vous venez. Si vous avez des idées, déposez-les dans les commentaires ci-dessous. Si vous avez rencontré des erreurs ou si vous n'avez pas compris quelque chose, faites-le moi savoir dans la section des commentaires afin que je puisse essayer de vous aider.

Rendez-vous dans la prochaine partie.