sql >> Databasteknik >  >> RDS >> Mysql

Användarkontohantering, roller, behörigheter, autentisering PHP och MySQL - Del 2

Detta är den andra delen av en serie om hanteringssystem för användarkonton, autentisering, roller, behörigheter. Du hittar den första delen här.

Databaskonfiguration

Skapa en MySQL-databas som heter användarkonton. Skapa sedan en fil i rotmappen för ditt projekt (mappen för användarkonton) och kalla den config.php. Den här filen kommer att användas för att konfigurera databasvariabler och sedan ansluta vår applikation till MySQL-databasen vi just skapade.

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

Vi har också startat sessionen eftersom vi kommer att behöva använda den senare för att lagra inloggad användarinformation som användarnamn. I slutet av filen definierar vi konstanter som hjälper oss att bättre hantera filen inkluderar.

Vår applikation är nu kopplad till MySQL-databasen. Låt oss skapa ett formulär som låter en användare ange sina uppgifter och registrera sitt konto. Skapa en signup.php-fil i projektets rotmapp:

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>

På den allra första raden i den här filen inkluderar vi config.php filen som vi skapade tidigare eftersom vi kommer att behöva använda INCLUDE_PATH-konstanten som config.php tillhandahåller i vår signup.php-fil. Genom att använda denna INCLUDE_PATH-konstant inkluderar vi även navbar.php, footer.php och userSignup.php som innehåller logiken för att registrera en användare i en databas. Vi kommer att skapa dessa filer mycket snart.

Nära slutet av filen finns ett runt fält där användaren kan klicka för att ladda upp en profilbild. När användaren klickar på det här området och väljer en profilbild från sin dator, visas först en förhandsgranskning av denna bild.

Denna bildförhandsvisning uppnås med jquery. När användaren klickar på knappen Ladda upp bild kommer vi att programmera utlösa filinmatningsfältet med JQuery och detta tar fram användarens datorfiler så att de kan bläddra i sin dator och välja sin profilbild. När de väljer bilden använder vi Jquery stillbild för att visa bilden tillfälligt. Koden som gör detta finns i vår display_profile_image.php-fil som vi kommer att skapa snart.

Visa inte i webbläsaren ännu. Låt oss först ge den här filen vad vi är skyldiga den. Låt oss för nu, i assets/css-mappen, skapa filen style.css som vi länkade i rubriken.

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

På den första raden i den här filen importerar vi ett Google-teckensnitt som heter 'Lora' för att få vår app att få ett vackrare typsnitt.

Nästa fil vi behöver i denna signup.php är filerna navbar.php och footer.php. Skapa dessa två filer i mappen includes/layouts:

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>

Den allra sista raden i filen signup.php länkar till ett JQuery-skript som heter display_profile_image.js och det gör precis vad dess namn säger. Skapa den här filen i assets/js mappen och klistra in den här koden i den:

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

Och sist, filen userSignup.php. Den här filen är där registreringsformulärets data skickas till för bearbetning och lagring i databasen. Skapa userSignup.php inuti includes/logic-mappen och klistra in den här koden i den:

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

Jag sparade den här filen till sist eftersom den hade mer arbete. Det första är att vi inkluderar ytterligare en fil som heter common_functions.php överst i den här filen. Vi inkluderar den här filen eftersom vi använder två metoder som kommer från den, nämligen:validateUser() och loginById() som vi kommer att skapa inom kort.

Skapa denna common_functions.php-fil i mappen 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";
        }
    }
  }

Låt mig uppmärksamma er på 2 viktiga funktioner i den här filen. De är: getSingleRecord() och getMultipleRecords(). Dessa funktioner är mycket viktiga eftersom var som helst i hela vår applikation, när vi vill välja en post från databasen, anropar vi bara funktionen getSingleRecord() och skickar SQL-frågan till den. Om vi ​​vill välja flera poster, du gissade rätt, kommer vi helt enkelt bara att anropa funktionen getMultipleRecords() och skicka den lämpliga SQL-frågan.

Dessa två funktioner tar tre parametrar, nämligen SQL-frågan, variabeltyperna (till exempel 's' betyder sträng, 'si' betyder sträng och heltal och så vidare) och slutligen en tredje parameter som är en matris med alla värden som frågan behöver för att kunna köras.

Om jag till exempel vill välja från användartabellen där användarnamnet är "John" och ålder 24, skriver jag bara min fråga så här:

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

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

I funktionsanropet representerar 's' strängtyp (eftersom användarnamnet 'John' är en sträng) och 'i' betyder heltal (ålder 20 är ett heltal). Denna funktion gör vårt arbete oerhört enkelt för om vi vill utföra en databasfråga på hundra olika platser i vår applikation, behöver vi inte bara dessa två rader. Funktionerna i sig har vardera cirka 8 - 10 rader kod så vi slipper repetera kod. Låt oss implementera dessa metoder på en gång.

config.php-filen kommer att inkluderas i varje fil där databasfrågor utförs eftersom den innehåller databaskonfiguration. Så det är den perfekta platsen att definiera dessa metoder. Öppna config.php igen och lägg bara till dessa metoder i slutet av filen:

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

Vi använder förberedda uttalanden och detta är viktigt av säkerhetsskäl.

Nu tillbaka till vår common_functions.php-fil igen. Den här filen har 4 viktiga funktioner som kommer att användas senare av många andra filer.

När användaren registrerar sig vill vi se till att de har tillhandahållit rätt data, så vi anropar funktionen validateUser() som den här filen tillhandahåller. Om en profilbild har valts laddar vi upp den genom att anropa uploadProfilePicture() funktionen, som den här filen tillhandahåller.

Om vi ​​lyckas spara användaren i databasen vill vi logga in dem omedelbart, så vi anropar funktionen loginById() som den här filen tillhandahåller. När en användare loggar in vill vi veta om de är admin eller normala, så vi anropar isAdmin() funktionen, som den här filen tillhandahåller. Om vi ​​upptäcker att de är admin (if isAdmin() returnerar true), omdirigerar vi dem till instrumentpanelen. Om vanliga användare omdirigerar vi till hemsidan.

Så du kan se att vår common_functions.php-fil är mycket viktig. Vi kommer att använda alla dessa funktioner när vi kommer att arbeta med vår adminsektion, vilket avsevärt minskar vårt arbete och undviker upprepning av kod.

För att göra det möjligt för användaren att registrera sig, låt oss skapa användartabellen. Men eftersom användartabellen är relaterad till rolltabellen kommer vi att skapa rolltabellen först.

rolltabell:

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

användartabell:

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
)

Användartabellen är relaterad till rolltabellen i en många-till-en-relation. När en roll tas bort från roller-tabellen vill vi att alla användare som tidigare hade det roll-id som attribut ska ha dess värde inställt på NULL. Detta innebär att användaren inte längre kommer att vara admin.

Om du skapar tabellen manuellt, gör klokt i att lägga till denna begränsning. Om du använder PHPMyAdmin kan du göra det genom att klicka på strukturfliken i användartabellen, sedan relationsvytabellen och sedan fylla i det här formuläret så här:

Vid det här laget tillåter vårt system en användare att registrera sig och sedan efter registrering loggas de automatiskt in. Men efter att ha loggat in, som visas i loginById() funktionen, omdirigeras de till startsidan (index.php). Låt oss skapa den sidan. Skapa en fil med namnet index.php i roten av programmet.

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

Öppna nu din webbläsare, gå till http://localhost/user-accounts/signup.php, fyll formuläret med lite testinformation (och kom ihåg dem eftersom vi kommer att använda användaren senare för att logga in), klicka sedan på registreringsknappen. Om allt gick bra kommer användaren att sparas i databasen och vår applikation kommer att omdirigeras till hemsidan.

På hemsidan kommer du att se ett fel som uppstår eftersom vi inkluderar messages.php-filen som vi inte har skapat ännu. Låt oss skapa det på en gång.

Skapa en fil med namnet messages.php:

i katalogen include/layouts

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

Uppdatera nu hemsidan och felet är borta.

Och det var allt för den här delen. I nästa del kommer vi att fortsätta med att validera registreringsformuläret, användarinloggning/utloggning och påbörja arbetet med adminsektionen. Det här låter som för mycket arbete, men tro mig, det är enkelt, särskilt som vi redan har skrivit en del kod som underlättar vårt arbete med Admin-sektionen.

Tack för att du följde med. Hoppas du följer med. Om du har några tankar, skriv dem i kommentarerna nedan. Om du stötte på några fel eller inte förstod något, låt mig veta i kommentarsfältet så att jag kan försöka hjälpa dig.

Vi ses i nästa del.


  1. Hur man importerar MySQL-databaser på kommandoraden

  2. Tillåt användare att bara komma åt vissa tabeller i min innehållsleverantör

  3. Varför att använda enhetstester är en stor investering i högkvalitativ arkitektur

  4. Beräkna antalet samtidiga händelser i SQL