Đây là phần thứ hai của loạt bài về hệ thống quản lý tài khoản người dùng, xác thực, vai trò, quyền. Bạn có thể tìm thấy phần đầu tiên tại đây.
Cấu hình cơ sở dữ liệu
Tạo cơ sở dữ liệu MySQL được gọi là tài khoản người dùng. Sau đó, trong thư mục gốc của dự án của bạn (thư mục tài khoản người dùng), tạo một tệp và gọi nó là config.php. Tệp này sẽ được sử dụng để định cấu hình các biến cơ sở dữ liệu và sau đó kết nối ứng dụng của chúng tôi với cơ sở dữ liệu MySQL mà chúng tôi vừa tạo.
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
?>
Chúng tôi cũng đã bắt đầu phiên này vì chúng tôi sẽ cần sử dụng nó sau này để lưu trữ thông tin người dùng đã đăng nhập như tên người dùng. Ở cuối tệp, chúng tôi đang xác định các hằng số sẽ giúp chúng tôi xử lý tốt hơn các tệp bao gồm.
Ứng dụng của chúng tôi hiện đã được kết nối với cơ sở dữ liệu MySQL. Hãy tạo một biểu mẫu cho phép người dùng nhập thông tin chi tiết và đăng ký tài khoản của họ. Tạo tệp signup.php trong thư mục gốc của dự án:
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>
Trên dòng đầu tiên của tệp này, chúng tôi bao gồm tệp config.php mà chúng tôi đã tạo trước đó vì chúng tôi sẽ cần sử dụng hằng số INCLUDE_PATH mà config.php cung cấp bên trong tệp signup.php của chúng tôi. Sử dụng hằng số INCLUDE_PATH này, chúng tôi cũng bao gồm navbar.php, footer.php và userSignup.php, giữ logic để đăng ký người dùng trong cơ sở dữ liệu. Chúng tôi sẽ sớm tạo các tệp này.
Gần cuối tệp, có một trường tròn nơi người dùng có thể nhấp vào để tải ảnh hồ sơ lên. Khi người dùng nhấp vào khu vực này và chọn hình ảnh hồ sơ từ máy tính của họ, bản xem trước của hình ảnh này sẽ được hiển thị đầu tiên.
Bản xem trước hình ảnh này đạt được với jquery. Khi người dùng nhấp vào nút tải lên hình ảnh, chúng tôi sẽ lập trình kích hoạt trường nhập tệp bằng JQuery và thao tác này hiển thị tệp máy tính của người dùng để họ duyệt máy tính và chọn hình ảnh hồ sơ của họ. Khi họ chọn hình ảnh, chúng tôi sử dụng Jquery tĩnh để hiển thị hình ảnh tạm thời. Mã thực hiện điều này được tìm thấy trong tệp display_profile_image.php mà chúng tôi sẽ sớm tạo.
Chưa xem trên trình duyệt. Đầu tiên chúng ta hãy cung cấp cho tập tin này những gì chúng ta nợ nó. Bây giờ, bên trong thư mục asset / css, hãy tạo tệp style.css mà chúng tôi đã liên kết trong phần 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; }
Trên dòng đầu tiên của tệp này, chúng tôi đang nhập một phông chữ của Google có tên là 'Lora' để làm cho ứng dụng của chúng tôi có phông chữ đẹp hơn.
Tệp tiếp theo chúng ta cần trong signup.php này là tệp navbar.php và footer.php. Tạo hai tệp này bên trong thư mục include / 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>
Dòng cuối cùng của tệp signup.php liên kết đến một tập lệnh JQuery có tên là display_profile_image.js và nó thực hiện đúng như tên gọi của nó. Tạo tệp này bên trong thư mục asset / js và dán mã này vào bên trong:
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);
}
});
});
});
Và cuối cùng là tệp userSignup.php. Tệp này là nơi dữ liệu biểu mẫu đăng ký được gửi đến để xử lý và lưu vào cơ sở dữ liệu. Tạo userSignup.php bên trong thư mục include / logic và dán mã này vào bên trong:
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";
}
}
}
Tôi đã lưu tệp này lần cuối vì nó còn nhiều việc phải làm. Điều đầu tiên là chúng tôi đang bao gồm một tệp khác có tên common_functions.php ở đầu tệp này. Chúng tôi đang bao gồm tệp này vì chúng tôi đang sử dụng hai phương thức đến từ nó, đó là:validateUser () và loginById () mà chúng tôi sẽ tạo trong thời gian ngắn.
Tạo tệp common_functions.php này trong thư mục include / logic của bạn:
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";
}
}
}
Hãy để tôi thu hút sự chú ý của bạn đến 2 chức năng quan trọng trong tệp này. Chúng là:getSingleRecord () và getMultipleRecords (). Các hàm này rất quan trọng vì ở bất kỳ đâu trong toàn bộ ứng dụng của chúng ta, khi chúng ta muốn chọn một bản ghi từ cơ sở dữ liệu, chúng ta sẽ chỉ gọi hàm getSingleRecord () và chuyển truy vấn SQL tới nó. Nếu chúng tôi muốn chọn nhiều bản ghi, bạn đoán vậy, chúng tôi sẽ chỉ gọi hàm getMultipleRecords () với việc chuyển truy vấn SQL thích hợp.
Hai hàm này nhận 3 tham số là truy vấn SQL, các kiểu biến (ví dụ:'s' có nghĩa là chuỗi, 'si' nghĩa là chuỗi và số nguyên, v.v.) và cuối cùng là tham số thứ ba là một mảng gồm tất cả các giá trị truy vấn cần để thực thi.
Ví dụ:Nếu tôi muốn chọn từ bảng người dùng có tên người dùng là 'John' và 24 tuổi, tôi sẽ chỉ cần viết truy vấn của mình như sau:
$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query $user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query
Trong lệnh gọi hàm, 's' đại diện cho kiểu chuỗi (vì tên người dùng 'John' là một chuỗi) và 'i' có nghĩa là số nguyên (tuổi 20 là số nguyên). Chức năng này làm cho công việc của chúng tôi trở nên vô cùng dễ dàng bởi vì nếu chúng tôi muốn thực hiện một truy vấn cơ sở dữ liệu ở hàng trăm vị trí khác nhau trong ứng dụng của mình, chúng tôi sẽ không chỉ có hai dòng này. Bản thân mỗi hàm có khoảng 8 - 10 dòng mã vì vậy chúng ta không cần phải lặp lại mã. Hãy triển khai các phương pháp này cùng một lúc.
Tệp config.php sẽ được bao gồm trong mọi tệp nơi các truy vấn cơ sở dữ liệu được thực hiện vì nó chứa cấu hình cơ sở dữ liệu. Vì vậy, nó là nơi hoàn hảo để xác định các phương pháp này. Mở config.php một lần nữa và chỉ cần thêm các phương thức này vào cuối tệp:
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;
}
Chúng tôi đang sử dụng các tuyên bố chuẩn bị sẵn và điều này quan trọng vì lý do bảo mật.
Bây giờ quay lại tệp common_functions.php của chúng tôi một lần nữa. Tệp này chứa 4 chức năng quan trọng mà sau này nhiều tệp khác sẽ sử dụng.
Khi người dùng đăng ký, chúng tôi muốn đảm bảo rằng họ đã cung cấp đúng dữ liệu, vì vậy chúng tôi gọi hàm validateUser () mà tệp này cung cấp. Nếu hình ảnh hồ sơ được chọn, chúng tôi tải nó lên bằng cách gọi hàm uploadProfilePicture (), mà tệp này cung cấp.
Nếu chúng tôi lưu thành công người dùng trong cơ sở dữ liệu, chúng tôi muốn đăng nhập họ ngay lập tức, vì vậy chúng tôi gọi hàm loginById () mà tệp này cung cấp. Khi người dùng đăng nhập, chúng tôi muốn biết họ là quản trị viên hay bình thường, vì vậy chúng tôi gọi hàm isAdmin () mà tệp này cung cấp. Nếu chúng tôi thấy rằng họ là quản trị viên (nếu isAdmin () trả về true), chúng tôi sẽ chuyển hướng họ đến trang tổng quan. Nếu người dùng bình thường, chúng tôi chuyển hướng đến trang chủ.
Vì vậy, bạn có thể thấy tệp common_functions.php của chúng tôi rất quan trọng. Chúng tôi sẽ sử dụng tất cả các chức năng này khi chúng tôi làm việc trên phần quản trị của mình, điều này giúp giảm thiểu công việc của chúng tôi và tránh lặp lại mã.
Để cho phép người dùng đăng ký, hãy tạo bảng người dùng. Nhưng vì bảng người dùng có liên quan đến bảng vai trò, chúng tôi sẽ tạo bảng vai trò trước.
bảng vai trò:
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
)
bảng người dùng:
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
)
Bảng người dùng có liên quan đến bảng vai trò trong mối quan hệ Nhiều-Một. Khi một vai trò bị xóa khỏi bảng vai trò, chúng tôi muốn tất cả người dùng trước đây có role_id đó làm thuộc tính của họ phải đặt giá trị của nó thành NULL. Điều này có nghĩa là người dùng sẽ không còn là quản trị viên.
Nếu bạn đang tạo bảng theo cách thủ công, hãy thực hiện tốt việc thêm ràng buộc này. Nếu bạn đang sử dụng PHPMyAdmin, bạn có thể làm điều đó bằng cách nhấp vào tab cấu trúc trên bảng người dùng, sau đó là bảng xem mối quan hệ, và cuối cùng điền vào biểu mẫu này như sau:
Tại thời điểm này, hệ thống của chúng tôi cho phép người dùng đăng ký và sau khi đăng ký, họ sẽ tự động đăng nhập. Nhưng sau khi đăng nhập, như được hiển thị trong hàm loginById (), họ được chuyển hướng đến trang chủ (index.php). Hãy tạo trang đó. Trong thư mục gốc của ứng dụng, hãy tạo một tệp có tên là 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") ?>
Bây giờ mở trình duyệt của bạn, truy cập http://localhost/user-accounts/signup.php, điền vào biểu mẫu với một số thông tin kiểm tra (và hãy nhớ chúng vì chúng tôi sẽ sử dụng người dùng sau này để đăng nhập), sau đó nhấp vào nút đăng ký. Nếu mọi việc suôn sẻ, người dùng sẽ được lưu trong cơ sở dữ liệu và ứng dụng của chúng tôi sẽ chuyển hướng đến Trang chủ.
Trên trang chủ, bạn sẽ thấy một lỗi phát sinh do chúng tôi đang đưa vào tệp tin messages.php mà chúng tôi chưa tạo. Hãy tạo nó ngay lập tức.
Trong thư mục include / layouts, hãy tạo một tệp có tên là messages.php:
message.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; ?>
Bây giờ hãy làm mới trang chủ và lỗi đã biến mất.
Và đó là nó cho phần này. Trong phần tiếp theo, chúng tôi sẽ tiếp tục xác thực biểu mẫu đăng ký, người dùng đăng nhập / đăng xuất và bắt đầu công việc trên phần quản trị. Điều này nghe có vẻ như quá nhiều công việc nhưng hãy tin tưởng tôi, nó rất đơn giản, đặc biệt là vì chúng tôi đã viết một số mã giúp giảm bớt công việc của chúng tôi trên phần Quản trị viên.
Cảm ơn vì đã theo dõi. Hy vọng bạn sẽ đến cùng. Nếu bạn có bất kỳ suy nghĩ nào, hãy thả chúng vào phần bình luận bên dưới. Nếu bạn gặp bất kỳ lỗi nào hoặc không hiểu điều gì đó, hãy cho tôi biết trong phần nhận xét để tôi có thể thử và giúp bạn.
Hẹn gặp lại các bạn trong phần tiếp theo.