Mysql
 sql >> база данни >  >> RDS >> Mysql

Управление на потребителски акаунт, роли, разрешения, удостоверяване PHP и MySQL - част 2

Това е втората част от поредица за система за управление на потребителски акаунти, удостоверяване, роли, разрешения. Можете да намерите първата част тук.

Конфигурация на базата данни

Създайте MySQL база данни, наречена user-accounts. След това в главната папка на вашия проект (папка user-accounts) създайте файл и го наречете config.php. Този файл ще се използва за конфигуриране на променливи на базата данни и след това за свързване на нашето приложение към MySQL базата данни, която току-що създадохме.

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

Също така започнахме сесията, защото ще трябва да я използваме по-късно, за да съхраняваме информация за влезли потребител, като потребителско име. В края на файла ние дефинираме константи, които ще ни помогнат да се справяме по-добре с включването на файлове.

Нашето приложение вече е свързано с базата данни MySQL. Нека създадем формуляр, който позволява на потребителя да въведе своите данни и да регистрира акаунта си. Създайте файл signup.php в основната папка на проекта:

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>

На първия ред в този файл включваме файла config.php, който създадохме по-рано, защото ще трябва да използваме константата INCLUDE_PATH, която config.php предоставя в нашия файл signup.php. Използвайки тази константа INCLUDE_PATH, ние също така включваме navbar.php, footer.php и userSignup.php, които съдържат логиката за регистриране на потребител в база данни. Ще създадем тези файлове много скоро.

Близо до края на файла има кръгло поле, където потребителят може да щракне, за да качи изображение на профил. Когато потребителят щракне върху тази област и избере изображение на профил от своя компютър, първо се показва визуализация на това изображение.

Тази визуализация на изображението се постига с jquery. Когато потребителят щракне върху бутона за качване на изображение, ние програмно ще задействаме полето за въвеждане на файл с помощта на JQuery и това извежда компютърните файлове на потребителя, за да може да прегледа компютъра си и да избере изображението на своя профил. Когато изберат изображението, ние използваме Jquery still, за да покажем изображението временно. Кодът, който прави това, се намира в нашия файл display_profile_image.php, който скоро ще създадем.

Все още не преглеждайте в браузъра. Нека първо дадем на този файл това, което дължим. Засега, в папката активи/css, нека създадем файла style.css, който свързахме в секцията 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; }

На първия ред от този файл импортираме шрифт на Google, наречен „Lora“, за да направим приложението ни по-красив шрифт.

Следващият файл, от който се нуждаем в този signup.php, са файловете navbar.php и footer.php. Създайте тези два файла в папката 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>

Последният ред на файла signup.php се свързва към JQuery скрипт, наречен display_profile_image.js и прави точно това, което казва името му. Създайте този файл в папката assets/js и поставете този код в него:

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

И накрая, файлът userSignup.php. Този файл е мястото, където данните от формуляра за регистрация се изпращат за обработка и запазване в базата данни. Създайте userSignup.php в папката includes/logic и поставете този код вътре:

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

Запазих този файл за последно, защото имаше повече работа. Първото нещо е, че включваме още един файл с име common_functions.php в горната част на този файл. Включваме този файл, защото използваме два метода, които идват от него, а именно:validateUser() и loginById(), които ще създадем скоро.

Създайте този файл common_functions.php във вашата папка 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";
        }
    }
  }

Позволете ми да насоча вниманието ви към 2 важни функции в този файл. Те са: getSingleRecord() и getMultipleRecords(). Тези функции са много важни, защото навсякъде в цялото ни приложение, когато искаме да изберем запис от базата данни, просто ще извикаме функцията getSingleRecord() и ще й предадем SQL заявката. Ако искаме да изберем няколко записа, познахте, просто ще извикаме и функцията getMultipleRecords() с подаване на съответната SQL заявка.

Тези две функции приемат 3 параметъра, а именно SQL заявката, типовете променливи (например, 's' означава низ, 'si' означава низ и цяло число и т.н.) и накрая трети параметър, който е масив от всички стойности, които заявката се нуждае, за да се изпълни.

Например, ако искам да избера от таблицата с потребители, където потребителското име е „John“ и възраст 24, просто ще напиша заявката си така:

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

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

В извикването на функцията 's' представлява низов тип (тъй като потребителското име 'John' е низ), а 'i' означава цяло число (възраст 20 е цяло число). Тази функция прави работата ни изключително лесна, защото ако искаме да изпълним заявка за база данни на сто различни места в нашето приложение, няма да ни се налага да правим само тези два реда. Всяка от самите функции има около 8 - 10 реда код, така че сме пощадени от повтаряне на код. Нека приложим тези методи наведнъж.

config.php файлът ще бъде включен във всеки файл, където се извършват заявки за база данни, тъй като съдържа конфигурация на базата данни. Така че това е идеалното място за дефиниране на тези методи. Отворете config.php още веднъж и просто добавете тези методи в края на файла:

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

Използваме подготвени изявления и това е важно от съображения за сигурност.

Сега отново се върнете към нашия common_functions.php файл. Този файл съдържа 4 важни функции, които ще бъдат използвани по-късно от много други файлове.

Когато потребителят се регистрира, искаме да се уверим, че е предоставил правилните данни, затова извикваме функцията validateUser() , която предоставя този файл. Ако е избрано изображение на потребителски профил, ние го качваме, като извикаме функцията uploadProfilePicture() , която предоставя този файл.

Ако успешно запазим потребителя в базата данни, искаме да го регистрираме незабавно, така че извикваме функцията loginById(), която предоставя този файл. Когато потребител влезе, искаме да знаем дали е администратор или нормален, затова извикваме функцията isAdmin() , която предоставя този файл. Ако установим, че са администратори (ако isAdmin() връща true), ги пренасочваме към таблото за управление. Ако са нормални потребители, ние пренасочваме към началната страница.

Така че можете да видите нашия файл common_functions.php е много важен. Ще използваме всички тези функции, когато работим върху нашия администраторски раздел, което значително намалява работата ни и избягва повторение на код.

За да позволим на потребителя да се регистрира, нека създадем таблицата на потребителите. Но тъй като таблицата с потребители е свързана с таблицата с роли, първо ще създадем таблицата с роли.

таблица на роли:

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

таблица потребители:

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
)

Таблицата потребители е свързана с таблицата роли в отношение „Много към едно“. Когато дадена роля бъде изтрита от таблицата с ролите, искаме всички потребители, които преди това са имали този role_id като свой атрибут, да имат стойността й, зададена на NULL. Това означава, че потребителят вече няма да бъде администратор.

Ако създавате таблицата ръчно, добре е да добавите това ограничение. Ако използвате PHPMyAdmin, можете да направите това, като щракнете върху раздела за структура в таблицата потребители, след това таблицата за изглед на релации и след това накрая попълните този формуляр по следния начин:

В този момент нашата система позволява на потребителя да се регистрира и след това след регистрация той автоматично влиза в системата. Но след влизане, както е показано във функцията loginById() , те се пренасочват към началната страница (index.php). Нека създадем тази страница. В корена на приложението създайте файл с име 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") ?>

Сега отворете браузъра си, отидете на http://localhost/user-accounts/signup.php, попълнете формуляра с тестова информация (и добре е да ги запомните, тъй като по-късно ще използваме потребителя за влизане), след което щракнете бутона за регистрация. Ако всичко е минало добре, потребителят ще бъде записан в базата данни и нашето приложение ще пренасочи към началната страница.

На началната страница ще видите грешка, която възниква, защото включваме файл messages.php, който все още не сме създали. Нека го създадем веднага.

В директорията include/layouts създайте файл с име 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; ?>

Сега опреснете началната страница и грешката е изчезнала.

И това е всичко за тази част. В следващата част ще продължим с валидирането на формуляра за регистрация, влизане/излизане на потребителя и започваме работа в администраторската секция. Това звучи като твърде много работа, но повярвайте ми, това е лесно, особено след като вече сме написали някакъв код, който улеснява работата ни в секцията Admin.

Благодаря, че следвате. Надявам се да дойдеш. Ако имате някакви мисли, пуснете ги в коментарите по-долу. Ако сте срещнали грешки или не сте разбрали нещо, уведомете ме в секцията за коментари, за да мога да се опитам да ви помогна.

Ще се видим в следващата част.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Мога ли да разреша това с чист mysql? (присъединяване към '' разделени стойности в колона)

  2. Как да конвертирам времеви отпечатък в дата и час в MySQL?

  3. Създайте нова таблица, като изберете данни от други таблици с CREATE TABLE AS

  4. Импортиране на CSV в MySQL с различен формат за дата

  5. Използвайте mysqldump за архивиране на MySQL или MariaDB