Toto je druhá část série o systému správy uživatelských účtů, autentizaci, rolích, oprávněních. První část najdete zde.
Konfigurace databáze
Vytvořte databázi MySQL nazvanou uživatelské účty. Poté v kořenové složce projektu (složka uživatelských účtů) vytvořte soubor a nazvěte jej config.php. Tento soubor bude použit ke konfiguraci databázových proměnných a následnému připojení naší aplikace k databázi MySQL, kterou jsme právě vytvořili.
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
?>
Také jsme zahájili relaci, protože ji budeme muset později použít k uložení informací o přihlášeném uživateli, jako je uživatelské jméno. Na konci souboru definujeme konstanty, které nám pomohou lépe zpracovávat obsah souboru.
Naše aplikace je nyní připojena k databázi MySQL. Pojďme vytvořit formulář, který umožní uživateli zadat své údaje a zaregistrovat svůj účet. Vytvořte soubor signup.php v kořenové složce projektu:
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>
Na úplně prvním řádku v tomto souboru uvádíme soubor config.php, který jsme vytvořili dříve, protože budeme muset použít konstantu INCLUDE_PATH, kterou config.php poskytuje v našem souboru signup.php. Pomocí této konstanty INCLUDE_PATH také zahrneme navbar.php, footer.php a userSignup.php, které drží logiku pro registraci uživatele v databázi. Tyto soubory vytvoříme velmi brzy.
Na konci souboru je kulaté pole, kam může uživatel kliknout a nahrát profilový obrázek. Když uživatel klikne na tuto oblast a vybere si profilový obrázek ze svého počítače, nejprve se zobrazí náhled tohoto obrázku.
Tento náhled obrázku je dosažen pomocí jquery. Když uživatel klikne na tlačítko nahrát obrázek, programově spustíme vstupní pole souboru pomocí JQuery a tím se zobrazí soubory v počítači uživatele, aby mohl procházet svůj počítač a vybrat si obrázek profilu. Když vyberou obrázek, použijeme Jquery still k dočasnému zobrazení obrázku. Kód, který to dělá, najdete v našem souboru display_profile_image.php, který brzy vytvoříme.
Zatím neprohlížejte v prohlížeči. Nejprve dáme tomuto souboru, co mu dlužíme. Prozatím ve složce aktiv/css vytvořte soubor style.css, který jsme propojili v sekci 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; }
Na první řádek tohoto souboru importujeme písmo Google s názvem „Lora“, aby naše aplikace měla krásnější písmo.
Další soubor, který potřebujeme v tomto signup.php, jsou soubory navbar.php a footer.php. Vytvořte tyto dva soubory ve složce 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>
Úplně poslední řádek souboru signup.php odkazuje na skript JQuery s názvem display_profile_image.js a dělá přesně to, co říká jeho název. Vytvořte tento soubor ve složce assets/js a vložte do něj tento kód:
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);
}
});
});
});
A nakonec soubor userSignup.php. Do tohoto souboru se odesílají data registračního formuláře ke zpracování a uložení do databáze. Vytvořte userSignup.php ve složce includes/logic a vložte do ní tento kód:
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";
}
}
}
Tento soubor jsem si uložil jako poslední, protože s ním bylo více práce. První věcí je, že na začátek tohoto souboru vkládáme ještě další soubor s názvem common_functions.php. Tento soubor zařazujeme, protože používáme dvě metody, které z něj pocházejí, konkrétně:validateUser() a loginById(), které brzy vytvoříme.
Vytvořte tento soubor common_functions.php ve složce 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";
}
}
}
Dovolte mi, abych vás upozornil na 2 důležité funkce v tomto souboru. Jsou to: getSingleRecord() a getMultipleRecords(). Tyto funkce jsou velmi důležité, protože kdekoli v celé naší aplikaci, když chceme vybrat záznam z databáze, pouze zavoláme funkci getSingleRecord() a předáme jí SQL dotaz. Pokud chceme vybrat více záznamů, uhodli jste, jednoduše zavoláme také funkci getMultipleRecords() s předáním příslušného SQL dotazu.
Tyto dvě funkce berou 3 parametry, konkrétně SQL dotaz, typy proměnných (například „s“ znamená řetězec, „si“ znamená řetězec a celé číslo atd.) a nakonec třetí parametr, který je polem všech hodnot, které dotaz potřebuje k provedení.
Například, pokud chci vybrat z tabulky uživatelů, kde je uživatelské jméno 'John' a věk 24, napíšu svůj dotaz takto:
$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query $user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query
Ve volání funkce 's' představuje typ řetězce (protože uživatelské jméno 'John' je řetězec) a 'i' znamená celé číslo (věk 20 je celé číslo). Tato funkce nám nesmírně usnadňuje práci, protože pokud chceme provést databázový dotaz na stovce různých míst v naší aplikaci, nebudeme muset pouze tyto dva řádky. Funkce samotné mají každá asi 8 - 10 řádků kódu, takže jsme ušetřeni opakování kódu. Pojďme tyto metody implementovat najednou.
config.php bude součástí každého souboru, kde se provádějí databázové dotazy, protože obsahuje konfiguraci databáze. Je to tedy ideální místo pro definování těchto metod. Otevřete config.php ještě jednou a přidejte tyto metody na konec souboru:
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;
}
Používáme připravená prohlášení a to je důležité z bezpečnostních důvodů.
Nyní zpět k našemu souboru common_functions.php znovu. Tento soubor obsahuje 4 důležité funkce, které později využije mnoho dalších souborů.
Když se uživatel zaregistruje, chceme se ujistit, že poskytl správná data, a proto zavoláme funkci validateUser() , kterou tento soubor poskytuje. Pokud byl vybrán profilový obrázek, nahrajeme jej voláním funkce uploadProfilePicture() , kterou tento soubor poskytuje.
Pokud uživatele úspěšně uložíme do databáze, chceme jej ihned přihlásit, zavoláme tedy funkci loginById() , kterou tento soubor poskytuje. Když se uživatel přihlásí, chceme vědět, zda je správce nebo normální, proto zavoláme funkci isAdmin() , kterou tento soubor poskytuje. Pokud zjistíme, že jsou admin (pokud isAdmin() vrátí true), přesměrujeme je na řídicí panel. Pokud jde o běžné uživatele, přesměrujeme je na domovskou stránku.
Takže můžete vidět, že náš soubor common_functions.php je velmi důležitý. Všechny tyto funkce využijeme, když budeme pracovat na naší administrátorské sekci, což nám značně ušetří práci a zabrání opakování kódu.
Chcete-li uživateli umožnit registraci, vytvořte tabulku uživatelů. Ale protože tabulka uživatelů souvisí s tabulkou rolí, nejprve vytvoříme tabulku rolí.
tabulka rolí:
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
)
tabulka uživatelů:
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
)
Tabulka uživatelů souvisí s tabulkou rolí ve vztahu Many-to-One. Když je role smazána z tabulky rolí, chceme, aby všichni uživatelé, kteří dříve měli toto role_id jako atribut, měli její hodnotu nastavenu na NULL. To znamená, že uživatel již nebude správcem.
Pokud tabulku vytváříte ručně, přidejte toto omezení. Pokud používáte PHPMyAdmin, můžete to udělat tak, že kliknete na kartu struktura v tabulce uživatelů, poté na tabulku zobrazení vztahů a nakonec vyplníte tento formulář takto:
V tomto okamžiku náš systém umožňuje uživateli zaregistrovat se a poté, co se zaregistruje, je automaticky přihlášen. Ale po přihlášení, jak ukazuje funkce loginById() , je přesměrován na domovskou stránku (index.php). Pojďme vytvořit tu stránku. V kořenovém adresáři aplikace vytvořte soubor s názvem 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") ?>
Nyní otevřete prohlížeč, přejděte na http://localhost/user-accounts/signup.php, vyplňte formulář několika testovacími informacemi (a dobře si je zapamatujte, protože uživatele použijeme později k přihlášení) a poté klikněte tlačítko pro přihlášení. Pokud vše proběhlo v pořádku, uživatel bude uložen do databáze a naše aplikace se přesměruje na domovskou stránku.
Na domovské stránce uvidíte chybu, která vzniká tím, že vkládáme soubor messages.php, který jsme ještě nevytvořili. Pojďme to vytvořit najednou.
V adresáři include/layouts vytvořte soubor s názvem 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">×</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">×</span></button>
<?php
echo $_SESSION['error_msg'];
unset($_SESSION['error_msg']);
?>
</div>
<?php endif; ?>
Nyní obnovte domovskou stránku a chyba je pryč.
A to je pro tento díl vše. V další části budeme pokračovat validací přihlašovacího formuláře, přihlášením/odhlášením uživatele a zahájíme práci na admin sekci. Zní to jako příliš mnoho práce, ale věřte mi, je to přímočaré, zvláště když jsme již napsali nějaký kód, který nám usnadňuje práci v sekci Správce.
Díky za sledování. Doufám, že přijdete. Pokud máte nějaké myšlenky, napište je do komentářů níže. Pokud jste narazili na nějaké chyby nebo něčemu nerozumíte, dejte mi vědět v sekci komentářů, abych se mohl pokusit vám pomoci.
Uvidíme se v další části.