To druga część serii o systemie zarządzania kontami użytkowników, uwierzytelnianiu, rolach, uprawnieniach. Pierwszą część znajdziesz tutaj.
Konfiguracja bazy danych
Utwórz bazę danych MySQL o nazwie konta użytkowników. Następnie w folderze głównym projektu (folder kont użytkowników) utwórz plik i nazwij go config.php. Ten plik zostanie użyty do skonfigurowania zmiennych bazy danych, a następnie połączenia naszej aplikacji z właśnie utworzoną bazą danych 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
?>
Rozpoczęliśmy również sesję, ponieważ będziemy musieli ją później wykorzystać do przechowywania informacji o zalogowanym użytkowniku, takich jak nazwa użytkownika. Na końcu pliku definiujemy stałe, które pomogą nam lepiej obsłużyć dołączenia plików.
Nasza aplikacja jest teraz połączona z bazą danych MySQL. Stwórzmy formularz, który pozwoli użytkownikowi wprowadzić swoje dane i zarejestrować swoje konto. Utwórz plik signup.php w folderze głównym projektu:
zapis.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>
W pierwszym wierszu tego pliku dołączamy plik config.php, który utworzyliśmy wcześniej, ponieważ będziemy musieli użyć stałej INCLUDE_PATH, którą udostępnia config.php w naszym pliku signup.php. Używając tej stałej INCLUDE_PATH, uwzględniamy również navbar.php, footer.php i userSignup.php, które przechowują logikę rejestrowania użytkownika w bazie danych. Wkrótce utworzymy te pliki.
Pod koniec pliku znajduje się okrągłe pole, w którym użytkownik może kliknąć, aby przesłać zdjęcie profilowe. Gdy użytkownik kliknie ten obszar i wybierze zdjęcie profilowe ze swojego komputera, najpierw zostanie wyświetlony podgląd tego obrazu.
Ten podgląd obrazu uzyskuje się za pomocą jquery. Gdy użytkownik kliknie przycisk przesyłania obrazu, programowo uruchomimy pole wejściowe pliku za pomocą JQuery, co spowoduje wyświetlenie plików komputera użytkownika, aby mógł przeglądać swój komputer i wybrać obraz profilu. Kiedy wybierają obraz, używamy Jquery nadal do tymczasowego wyświetlania obrazu. Kod, który to robi, znajduje się w naszym pliku display_profile_image.php, który wkrótce utworzymy.
Nie wyświetlaj jeszcze w przeglądarce. Najpierw dajmy temu plikowi to, co mu zawdzięczamy. Na razie w folderze asset/css utwórzmy plik style.css, do którego połączyliśmy się w sekcji 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; }
W pierwszym wierszu tego pliku importujemy czcionkę Google o nazwie „Lora”, aby nasza aplikacja miała piękniejszą czcionkę.
Następnym plikiem, którego potrzebujemy w tym signup.php, są pliki navbar.php i footer.php. Utwórz te dwa pliki w folderze dołączeń/układów:
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>
Ostatni wiersz pliku signup.php prowadzi do skryptu JQuery o nazwie display_profile_image.js i robi dokładnie to, co mówi jego nazwa. Utwórz ten plik w folderze asset/js i wklej do niego ten kod:
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);
}
});
});
});
I na koniec plik userSignup.php. W tym pliku przesyłane są dane z formularza rejestracji w celu przetworzenia i zapisania w bazie danych. Utwórz userSignup.php w folderze includes/logic i wklej do niego ten kod:
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";
}
}
}
Zapisałem ten plik na koniec, ponieważ miał więcej pracy. Po pierwsze, dołączamy kolejny plik o nazwie common_functions.php na górze tego pliku. Uwzględniamy ten plik, ponieważ używamy dwóch metod, które z niego pochodzą, a mianowicie:validateUser() i loginById(), które wkrótce utworzymy.
Utwórz ten plik common_functions.php w swoim folderze 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";
}
}
}
Pozwólcie, że zwrócę uwagę na 2 ważne funkcje w tym pliku. Są to:getSingleRecord() i getMultipleRecords(). Te funkcje są bardzo ważne, ponieważ w dowolnym miejscu naszej aplikacji, gdy chcemy wybrać rekord z bazy danych, po prostu wywołamy funkcję getSingleRecord() i przekażemy do niej zapytanie SQL. Jeśli chcemy wybrać wiele rekordów, zgadliście, po prostu wywołamy również funkcję getMultipleRecords() z przekazaniem odpowiedniego zapytania SQL.
Te dwie funkcje przyjmują 3 parametry, a mianowicie zapytanie SQL, typy zmiennych (na przykład „s” oznacza ciąg, „si” oznacza ciąg i liczbę całkowitą itd.) i wreszcie trzeci parametr, który jest tablicą wszystkich wartości, które zapytanie musi zostać wykonane.
Na przykład, jeśli chcę wybrać z tabeli użytkowników, w której nazwa użytkownika to „Jan” i ma 24 lata, po prostu napiszę zapytanie w ten sposób:
$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query
$user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query
W wywołaniu funkcji „s” reprezentuje typ ciągu (ponieważ nazwa użytkownika „Jan” jest ciągiem), a „i” oznacza liczbę całkowitą (20 lat jest liczbą całkowitą). Ta funkcja niezwykle ułatwia nam pracę, ponieważ chcąc wykonać zapytanie do bazy danych w stu różnych miejscach naszej aplikacji, nie będziemy musieli wykonywać tylko tych dwóch wierszy. Same funkcje mają po około 8-10 linijek kodu, dzięki czemu nie musimy powtarzać kodu. Zaimplementujmy te metody od razu.
Plik config.php będzie dołączany do każdego pliku, w którym wykonywane są zapytania do bazy danych, ponieważ zawiera konfigurację bazy danych. Jest to więc idealne miejsce na zdefiniowanie tych metod. Otwórz ponownie config.php i po prostu dodaj te metody na końcu pliku:
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;
}
Korzystamy z przygotowanych oświadczeń i jest to ważne ze względów bezpieczeństwa.
Wróćmy teraz ponownie do naszego pliku common_functions.php. Ten plik zawiera 4 ważne funkcje, które będą później używane przez wiele innych plików.
Gdy użytkownik się rejestruje, chcemy się upewnić, że podał prawidłowe dane, dlatego wywołujemy funkcję ValidateUser(), którą udostępnia ten plik. Jeśli wybrano zdjęcie profilowe, przesyłamy je, wywołując funkcję uploadProfilePicture() dostępną w tym pliku.
Jeśli pomyślnie zapiszemy użytkownika w bazie danych, chcemy go natychmiast zalogować, więc wywołujemy funkcję loginById(), którą udostępnia ten plik. Gdy użytkownik się loguje, chcemy wiedzieć, czy jest administratorem, czy normalnym użytkownikiem, dlatego wywołujemy funkcję isAdmin() dostępną w tym pliku. Jeśli stwierdzimy, że są adminami (jeśli isAdmin() zwraca true), przekierowujemy ich do pulpitu nawigacyjnego. W przypadku zwykłych użytkowników przekierowujemy na stronę główną.
Jak widać, nasz plik common_functions.php jest bardzo ważny. Użyjemy wszystkich tych funkcji, gdy będziemy pracować nad naszą sekcją administracyjną, co znacznie zmniejsza naszą pracę i pozwala uniknąć powtarzania kodu.
Aby umożliwić użytkownikowi rejestrację, utwórzmy tabelę użytkowników. Ale ponieważ tabela użytkowników jest powiązana z tabelą ról, najpierw utworzymy tabelę ról.
tabela ról:
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
)
tabela użytkowników:
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
)
Tabela użytkowników jest powiązana z tabelą ról w relacji wiele do jednego. Gdy rola zostanie usunięta z tabeli ról, chcemy, aby wszyscy użytkownicy, którzy wcześniej mieli ten identyfikator roli jako atrybut, mieli ustawioną wartość NULL. Oznacza to, że użytkownik nie będzie już administratorem.
Jeśli tworzysz tabelę ręcznie, dodaj to ograniczenie. Jeśli używasz PHPMyAdmin, możesz to zrobić, klikając kartę struktury w tabeli użytkowników, następnie tabelę widoku relacji, a następnie wypełniając ten formularz w ten sposób:
W tym momencie nasz system pozwala użytkownikowi na rejestrację, a następnie po zarejestrowaniu jest on automatycznie logowany. Jednak po zalogowaniu, jak pokazuje funkcja loginById(), zostaje przekierowany na stronę główną (index.php). Stwórzmy tę stronę. W katalogu głównym aplikacji utwórz plik o nazwie 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") ?>
Teraz otwórz przeglądarkę, wejdź na http://localhost/user-accounts/signup.php, wypełnij formularz danymi testowymi (i dobrze je zapamiętaj, ponieważ później użyjemy użytkownika do zalogowania), a następnie kliknij przycisk rejestracji. Jeśli wszystko poszło dobrze, użytkownik zostanie zapisany w bazie danych, a nasza aplikacja przekieruje na stronę główną.
Na stronie głównej zobaczysz błąd, który pojawia się, ponieważ dołączamy plik wiadomości.php, którego jeszcze nie utworzyliśmy. Stwórzmy to od razu.
W katalogu include/layouts utwórz plik o nazwie messages.php:
wiadomości.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; ?>
Teraz odśwież stronę główną, a błąd zniknie.
I to wszystko w tej części. W kolejnej części będziemy kontynuować walidację formularza rejestracji, logowanie/wylogowanie użytkownika oraz rozpoczynamy pracę nad sekcją administracyjną. Brzmi to zbyt dużo pracy, ale uwierz mi, jest to proste, zwłaszcza, że napisaliśmy już trochę kodu, który ułatwia nam pracę w sekcji Admin.
Dziękuję za śledzenie. Mam nadzieję, że nadchodzisz. Jeśli masz jakieś myśli, upuść je w komentarzach poniżej. Jeśli napotkałeś jakieś błędy lub czegoś nie zrozumiałeś, daj mi znać w sekcji komentarzy, abyśmy mogli spróbować Ci pomóc.
Do zobaczenia w następnej części.