Esta é a segunda parte de uma série sobre sistema de gerenciamento de contas de usuários, autenticação, funções, permissões. Você pode encontrar a primeira parte aqui.
Configuração do banco de dados
Crie um banco de dados MySQL chamado user-accounts. Em seguida, na pasta raiz do seu projeto (pasta user-accounts), crie um arquivo e chame-o de config.php. Este arquivo será usado para configurar as variáveis do banco de dados e então conectar nossa aplicação ao banco de dados MySQL que acabamos de criar.
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
?>
Também iniciamos a sessão porque precisaremos usá-la mais tarde para armazenar informações do usuário logado, como nome de usuário. No final do arquivo, estamos definindo constantes que nos ajudarão a lidar melhor com as inclusões de arquivos.
Nosso aplicativo agora está conectado ao banco de dados MySQL. Vamos criar um formulário que permita que um usuário insira seus dados e registre sua conta. Crie um arquivo signup.php na pasta raiz do projeto:
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 primeira linha deste arquivo, estamos incluindo o arquivo config.php que criamos anteriormente porque precisaremos usar a constante INCLUDE_PATH que o config.php fornece dentro do nosso arquivo signup.php. Usando essa constante INCLUDE_PATH, também incluímos navbar.php, footer.php e userSignup.php, que contém a lógica para registrar um usuário em um banco de dados. Vamos criar esses arquivos muito em breve.
Perto do final do arquivo, há um campo redondo onde o usuário pode clicar para fazer upload de uma imagem de perfil. Quando o usuário clica nessa área e seleciona uma imagem de perfil em seu computador, uma visualização dessa imagem é exibida primeiro.
Esta visualização da imagem é obtida com jquery. Quando o usuário clicar no botão de upload de imagem, acionaremos programaticamente o campo de entrada de arquivo usando JQuery e isso exibirá os arquivos do computador do usuário para que ele navegue em seu computador e escolha sua imagem de perfil. Quando eles selecionam a imagem, usamos Jquery still para exibir a imagem temporariamente. O código que faz isso é encontrado em nosso arquivo display_profile_image.php que criaremos em breve.
Não visualize no navegador ainda. Vamos primeiro dar a este arquivo o que devemos a ele. Por enquanto, dentro da pasta assets/css, vamos criar o arquivo style.css que vinculamos na seção 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 primeira linha deste arquivo, estamos importando uma fonte do Google chamada 'Lora' para deixar nosso aplicativo com uma fonte mais bonita.
O próximo arquivo que precisamos neste signup.php são os arquivos navbar.php e footer.php. Crie estes dois arquivos dentro da pasta 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>
A última linha do arquivo signup.php é vinculada a um script JQuery chamado display_profile_image.js e faz exatamente o que o nome diz. Crie este arquivo dentro da pasta assets/js e cole este código dentro dela:
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);
}
});
});
});
E por último, o arquivo userSignup.php. Este arquivo é para onde os dados do formulário de inscrição são enviados para processamento e salvamento no banco de dados. Crie userSignup.php dentro da pasta include/logic e cole este código dentro dela:
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";
}
}
}
Eu salvei este arquivo por último porque tinha mais trabalho para ele. A primeira coisa é que estamos incluindo outro arquivo chamado common_functions.php no topo deste arquivo. Estamos incluindo este arquivo porque estamos usando dois métodos que vêm dele, a saber:validateUser() e loginById() que criaremos em breve.
Crie este arquivo common_functions.php na sua pasta 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";
}
}
}
Deixe-me chamar sua atenção para 2 funções importantes neste arquivo. São eles: getSingleRecord() e getMultipleRecords(). Essas funções são muito importantes porque em qualquer lugar em todo o nosso aplicativo, quando queremos selecionar um registro do banco de dados, basta chamar a função getSingleRecord() e passar a consulta SQL para ela. Se quisermos selecionar vários registros, você adivinhou, simplesmente chamaremos a função getMultipleRecords() também com a passagem da consulta SQL apropriada.
Essas duas funções recebem 3 parâmetros, a saber, a consulta SQL, os tipos de variáveis (por exemplo, 's' significa string, 'si' significa string e inteiro, e assim por diante) e por último um terceiro parâmetro que é uma matriz de todos os valores que a consulta precisa para ser executada.
Por exemplo, se eu quiser selecionar na tabela de usuários onde o nome de usuário é 'John' e idade 24, vou escrever minha consulta assim:
$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query $user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query
Na chamada de função, 's' representa o tipo de string (já que o nome de usuário 'John' é uma string) e 'i' significa inteiro (idade 20 é um inteiro). Essa função facilita imensamente nosso trabalho, pois se quisermos realizar uma consulta de banco de dados em cem lugares diferentes em nossa aplicação, não precisaremos apenas dessas duas linhas. As funções em si têm cerca de 8 a 10 linhas de código, portanto, somos poupados de repetir o código. Vamos implementar esses métodos de uma vez.
O arquivo config.php será incluído em todos os arquivos onde as consultas ao banco de dados forem realizadas, pois ele contém a configuração do banco de dados. Portanto, é o lugar perfeito para definir esses métodos. Abra o config.php mais uma vez e adicione estes métodos ao final do arquivo:
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;
}
Estamos usando declarações preparadas e isso é importante por motivos de segurança.
Agora de volta ao nosso arquivo common_functions.php novamente. Este arquivo contém 4 funções importantes que serão usadas posteriormente por muitos outros arquivos.
Quando o usuário se registra, queremos ter certeza de que ele forneceu os dados corretos, então chamamos a função validateUser() , fornecida por esse arquivo. Se uma imagem de perfil foi selecionada, nós a carregamos chamando a função uploadProfilePicture() , que este arquivo fornece.
Se salvarmos o usuário com sucesso no banco de dados, queremos fazer login nele imediatamente, então chamamos a função loginById(), que esse arquivo fornece. Quando um usuário faz login, queremos saber se ele é administrador ou normal, então chamamos a função isAdmin() , fornecida por esse arquivo. Se descobrirmos que eles são admin (se isAdmin() retornar true), nós os redirecionamos para o painel. Se usuários normais, redirecionamos para a página inicial.
Então você pode ver que nosso arquivo common_functions.php é muito importante. Usaremos todas essas funções quando estivermos trabalhando em nossa seção de administração, o que reduz muito nosso trabalho e evita a repetição de código.
Para permitir que o usuário se cadastre, vamos criar a tabela de usuários. Mas como a tabela de usuários está relacionada à tabela de funções, criaremos a tabela de funções primeiro.
tabela de funções:
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
)
tabela de usuários:
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
)
A tabela de usuários está relacionada à tabela de funções em uma relação de muitos para um. Quando um papel é excluído da tabela de papéis, queremos que todos os usuários que tenham anteriormente esse role_id como atributo tenham o valor definido como NULL. Isso significa que o usuário não será mais admin.
Se você estiver criando a tabela manualmente, adicione essa restrição. Se você estiver usando o PHPMyAdmin, você pode fazer isso clicando na guia de estrutura na tabela de usuários, depois na tabela de visualização de relação e, finalmente, preenchendo este formulário assim:
Nesse ponto, nosso sistema permite que um usuário se registre e, após o registro, ele faça login automaticamente. Mas após o login, conforme mostrado na função loginById() , ele é redirecionado para a página inicial (index.php). Vamos criar essa página. Na raiz do aplicativo, crie um arquivo chamado 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") ?>
Agora abra seu navegador, acesse http://localhost/user-accounts/signup.php, preencha o formulário com algumas informações de teste (e lembre-se delas, pois usaremos o usuário mais tarde para fazer login) e clique em o botão de inscrição. Se tudo der certo, o usuário será salvo no banco de dados e nosso aplicativo redirecionará para a página inicial.
Na página inicial, você verá um erro que surge porque estamos incluindo o arquivo messages.php que ainda não criamos. Vamos criá-lo de uma vez.
No diretório includes/layouts, crie um arquivo chamado messages.php:
mensagens.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; ?>
Agora atualize a página inicial e o erro desapareceu.
E é isso para esta parte. Na próxima parte continuaremos com a validação do formulário de inscrição, login/logout do usuário e começaremos a trabalhar na seção de administração. Isso parece muito trabalho, mas acredite, é simples, especialmente porque já escrevemos alguns códigos que facilitam nosso trabalho na seção Admin.
Obrigado por seguir. Espero que você esteja vindo junto. Se você tiver alguma opinião, deixe-a nos comentários abaixo. Se você encontrou algum erro ou não entendeu alguma coisa, informe-nos na seção de comentários para que eu possa tentar ajudá-lo.
Nos vemos na próxima parte.