Lit RESET will add a password reset feature to the Lit SITE project. Users will be able to request a password reset and receive a reset link in their e-mail.
You can open a completed version of the project in your browser here.
If you want to download completed versions of the PHP scripts for this project, you can find them here.
Copy the project files from Lit SITE and put them in a new project folder. I recommend you name the project folder "n413_litreset"
Familiarize yourself with the the project structure. The password reset feature will focus on the Auth.php controller, the Auth_model.php model, and the login.php view (found at views/auth/login.php).
Here are the main steps for building the Lit RESET project:This requires 3 new methods in the Auth controller, and 3 new methods in the Auth_model model. This project will use a few new techniques: a Bootstrap modal, create a "token" using encryption, and will send e-mail from a server using PHP. Let's get started!
Project Set-up. Set up a directory in the htdocs folder to hold your project. The recommended name is n413_litreset.
Copy all the files from the Lit SITE project into your project folder. Or you can use the completed set of Lit SITE files from here. Either set should contain a complete set of the CodeIgniter files. Note: The CodeIgniter file structure includes a directory named "application". When we refer to files with path names, they are generally in relation to the "application" folder.
Open config/config.php and update the "base_url" value with the new project path. It should be:
$config['base_url'] = 'http://localhost:8888/n413_litreset/'; //or for XAMPP: //$config['base_url'] = 'http://localhost/n413_litreset/'; $config['from'] = '<your-account>@<your-server-domain>';
The "base_url" item is a path to the root of the project, and will be changed whenever you move the project.
The "from" item is for the e-mail feature of the project, and will hold your return address for the email message. It must be an account on the server where the project is running. Localhost development setups can't generally send email, so this is only important when you move the project to a webserver. If you know the server's domain name (such as "in-info-web4.informatics.iupui.edu"), go ahead and put your account name and the domain name in the "from" variable.
If you downloaded a new set of files, be sure the config/database.php files have been updated with the correct paths and database connection credentials, port numbers, etc. (See Lit LIST.)
We will use the same database tables ("list", "form_responses", "users_hash", and "litlist") used in The LIST, The FORM, The PASSWORD, and Lit LIST project. If you don't already have those tables set up, see the intructions here. Or you can download the SQL script to create the tables here.
"Forgot" link. Add a "Forgot Password" link below the login form in views/auth/login.php. The link should be an <a> tag, with a data attribute of data-toggle="modal" and an "href" attribute of href="#forgotModal". This will open a Bootstrap modal, which we will add below the login form.
(views/auth/login.php) ... <form id="login_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/><br/> Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> <a data-toggle="modal" href="#forgotModal">Forgot Password?</a> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> ...
Check your project in a browser to see the "Forgot Password" link below the log-in form.
Bootstrap Modal. A Bootstrap modal is a combination of HTML markup, CSS, and the Bootstrap Javascript code, so be sure you have all the Bootstrap files in place (including jQuery) if you have any trouble.
The Bootstrap modal has a complex structure that must be properly set up for the modal to work. The first time you build the structure it seems very complicated, but once you have the first one, you simply modify it to build all the modals that follow. Here is the structure:
There is a closing </body> tag between the markup for the login form and the </script> tag. Move the </body> tag below the end of the script, as we will add the modal code after the login form's closing </script> tag, but before the closing </body> tag. Here is the structure of the Bootstrap modal, without the <form> tag content that will go in the modal body:
(views/auth/login.php) ... <!-- -------------------------- AMP JAM RESET Reset Password Modal ------------------------- --> <div class="modal fade" id="forgotModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">AMP JAM RESET Reset Password</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!-- /.modal-header --> <div class="modal-body"> <!-- form markup goes here --> </div> <!-- /.modal-body --> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> <!-- /.modal-footer --> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> <!-- -------------------------- end Reset Password Modal ------------------------------ -->
If you test the code in a browser, you should see this when you click the "Forgot" link.
E-mail Form Create a form for entering the user's email address. Place it in the modal-body section of the modal markup. Use Bootstrap form classes for styling the form (form-horizontal, form-label, form-control), and be sure to provide placeholder divs for error messages. We will use jQuery and AJAX to submit the form, so leave the form's method and action attributes empty.
(views/auth/login.php) ... <form id="reset_form" name="reset_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-12"> <div class="row" style="padding:2em;"> <div class="form-group"> <label for="email" class="control-label">Enter your E-mail:</label> <input type="text" id="email" name="email" class="form-control" placeholder="E-mail address"> <div id="email_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> </div> <!-- /.row --> <div class="row row-gap"> <div class="col-11"> <button type="submit" class="btn btn-primary float-right">Reset Password</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-11 --> </div> <!-- /.row row-gap --> </div> <!-- /col-12 --> </div> <!-- /.row --> </form>
Insert this code into the modal-body section of the modal and test it in your browser. It should look like the screen shot to the right.
Last, add another <script> tag after the modal code to handle the form submission and AJAX. Begin with the submit function and the necessary preventDefault() method to avoid reloading a new page when the submit button is clicked. Then begin the jQuery $.post() function to handle the AJAX call. Remember the $.post() function's four arguments:
The URL will point to a method in the Auth.php controller, which is not yet in place. It will be called "send_reset_link". This method will be developed in a later step. The callback function will display any success or error messages returned from the server, targeting the placeholder divs in the form.
(views/auth/login.php) ... <script type="text/javascript"> // Attach a submit handler to the form $( "#reset_form" ).submit(function( event ) { event.preventDefault(); $.post("<?=site_url()?>/auth/send_reset_link", {email:$("#email").val()}, function(data){ //reset the error messages $("#user_message").html(""); $("#user_message").css("display","none"); $("#email_error").html(""); $("#email_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.email_error != null){ $("#email_error").html(data.email_error); $("#email_error").css("display","block"); } } }, "json" ); //post });//submit </script>
Below is the complete script for views/auth/login.php. You will not be able to test the form submission until the remaining steps scripts are in place.
(views/auth/login.php) <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>Full Stack Amp Jam Log-in</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="login_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/><br/> Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> <a data-toggle="modal" href="#forgotModal">Forgot Password?</a> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> <script> var this_page = "login"; var page_title = 'AMP JAM Site | Login'; $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#login_form").submit(function(event){ event.preventDefault(); $.post("n413auth.php", $("#login_form").serialize(), function(data){ //handle messages here if(data.status){ $("#form-container").html(data.success); right_navbar_update(data.role); }else{ $("#form-container").html(data.failed); } }, "json" ); //post }); //submit }); //document.ready function right_navbar_update(role){ var html = ""; if (role > 0) { html = '<li id="messages_item" class="nav-item">'+ '<a id="messages_link" class="nav-link" href="messages.php">Messages</a>'+ '</li>'; } html += '<li id="logout_item" class="nav-item">'+ '<a id="logout_link" class="nav-link" href="logout.php">Log-Out</a>'+ '</li>'; $("#right_navbar").html(html); } </script> <!-- -------------------------- AMP JAM RESET Reset Password Modal ------------------------- --> <div class="modal fade" id="forgotModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">AMP JAM RESET Reset Password</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!-- /.modal-header --> <div class="modal-body"> <form id="reset_form" name="reset_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-12"> <div class="row" style="padding:2em;"> <div class="form-group"> <label for="email" class="control-label">Enter your E-mail:</label> <input type="text" id="email" name="email" class="form-control" placeholder="E-mail address"> <div id="email_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> </div> <!-- /.row --> <div class="row row-gap"> <div class="col-11"> <button type="submit" class="btn btn-primary float-right">Reset Password</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-11 --> </div> <!-- /.row row-gap --> </div> <!-- /col-12 --> </div> <!-- /.row --> </form> </div> <!-- /.modal-body --> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> <!-- /.modal-footer --> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> <!-- -------------------------- end Reset Password Modal ------------------------------ --> <script type="text/javascript"> // Attach a submit handler to the form $( "#reset_form" ).submit(function( event ) { event.preventDefault(); $.post("<?=site_url()?>/auth/send_reset_link", {email:$("#email").val()}, function(data){ //reset the error messages $("#user_message").html(""); $("#user_message").css("display","none"); $("#email_error").html(""); $("#email_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.email_error != null){ $("#email_error").html(data.email_error); $("#email_error").css("display","block"); } } }, "json" ); //post });//submit </script> </body> </html>
Security. Resetting a user's password creates an opportunity for a security breach. There must be some way to verify the identity of the user before allowing the password to be reset. There are three main methods of doing this:
We will use the e-mail method because it is relatively simple and effective. However, it does require that your PHP scripts are running on a server capable of sending e-mail, and that the e-mail can actually reach the user. E-mail spam is such a large problem that network e-mail servers often filter out mail that is not sent from a recognized source. In some cases, e-mail sending servers must have a "reputation score" high enough to make it through the filter. For that reason, some people who need to have a reliable method of sending e-mail use e-mail relay servers that maintain the reputation scores. Within the IUPUI system, we should be able to send e-mail from the web-4 server and receive it on IU accounts, using the IU mail relay. Whether the message will make it through to other systems can be somewhat questionable. You generally cannot expect e-mail to be sent or received from your localhost server, such as MAMP or XAMPP.
Password Reset Log. There is one additional database table you will need for this project. To keep records of password reset requests, create a password reset log using a database table. The table should have four columns:
Here is what the table structure should look like:
Here is what the data table looks like:
send_reset_link This feature will accept the user's email address input, check to see if it is in the database, then send a reset link (and a security token) to the user's email. This functionality is spread across the model-view-controller design pattern in the following way:
First, open controllers/Auth.php and add a method (function) named "send_reset_link()". Be sure you add the method inside the Auth class. This method will need to do the following things:
The controller method looks like this:
(views/auth/login.php) ... public function send_reset_link(){ $email = $this->input->post('email'); $messages = $this->auth_model->send_reset_link($email); echo json_encode($messages); }
One thing to note: The CodeIgniter Input class will do much of the data sanitizing automatically, so it is not necessary to do so many sanitizing steps.
Next, open models/Auth_model.php and create a new method inside the Auth_model class. This method will be more complex. It will do the following things:
Begin by setting up the $messages variable, and verifying the email address:
(models/Auth_model/send_reset_link) ... public function send_reset_link($email){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["email_error"] = ""; $messages["user_message"] = ""; $user_id = 0; $email = trim($email); //check for an empty username if (strlen($email) < 1){ $messages["errors"] = true; $messages["email_error"] = 'You must enter your email address.'; } // if (strlen($email < 1) //check for valid email if (!filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = true; $messages["email_error"] = 'You must enter a valid email address.'; }else{ // if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = stripslashes($email); $email = strip_tags($email); $email = $this->db->escape_str($email); } // -end else- if (!filter_var($email, FILTER_VALIDATE_EMAIL))
The filter_var() function that checks for a valid e-mail is one of the PHP sanitizing methods. You can learn more about PHP filters here: https://www.w3schools.com/php/php_form_url_email.asp.
Note that this section of code is very similar to the classic PHP version (The RESET), but is somewhat simpler, without the need to check whether variables exist and some decoding steps. One important difference is with the "escape" step. The classic PHP version directly calls the mysqli library and references the $link variable for the escape step. With CodeIgniter, the database processes are abstracted and the db class is called instead. The db class has two methods for escaping strings: escape() and escape_str(). The escape() method places single quotes around the output, and the escape_str() method does not. When it is time to compose the SQL query, it is important to know whether the string is wrapped in single quotes or not.
Now check the users_hash table to see if the email is found in any of the records. If it is, create the token and insert a record into the password_reset_log.
(models/Auth_model/send_reset_link) ... if(! $messages["errors"] ){ $sql = "SELECT id FROM users_hash WHERE email = '".$email."' "; $query = $this->db->query($sql); if($query->num_rows() == 1){ $row = $query->row_array(); $user_id = $row["id"]; $token = sha1($email.time()); $sql = "INSERT INTO `password_reset_log` (`id`, `user_id`, `reset_token`, `timestamp`) VALUES (NULL, '".$user_id."', '".$token."', NOW())"; $query = $this->db->query($sql);
CodeIgniter abstracts the query method as well, using the query class. The num_rows() method and the row_array() method are part of the query class. The row_array() method outputs the first row of the query result as an associative array.
The "token" is generated by concatenating the user's email with the output of the PHP time() function (a UNIX time number representing the number of seconds since Jan. 1, 1970). This produces a constantly changing unique string, which will be "hashed" by the sha1() encryption function. The result is a unique and complex string that can be used to verify the password reset link in the coming steps.
The NOW() function in the INSERT query is a MySQL function that produces a current timestamp.
The next step sends the e-mail message to the user.
(models/Auth_model/send_reset_link) ... if($this->db->affected_rows() == 1){ //define the headers $to = $email; $from = config_item('from'); //is defined in the "config" file $subject = 'Password Reset Request'; $message_text = ' A password reset request has been made for your AMP JAM account that uses this e-mail address. If you did not initiate this request, please notify the security team at once. If you made the request, please click on the link below to reset your password. This link will expire one hour from the time this e-mail was sent. '.site_url().'/auth/verify_link/'.$token; //this should be a path to the Auth controller's verify_link method. The token is the 3rd URL segment //be sure the /r/n (carriage return) characters are in DOUBLE QUOTES! //PHP treats single quoted escaped characters differently, and things will break $headers = 'From: '.$from . "\r\n" . 'Reply-To: '.$from . "\r\n" . 'X-Mailer: PHP/' . phpversion(); mail($to, $subject, $message_text, $headers); }else{ $messages["errors"] = true; $messages["email_error"] = "There was a problem with the database. Your password cannot be reset."; } }else{ // else - if($this->db->affected_rows() == 1) $messages["email_error"] = "The e-mail address you entered was not found in the database.
Check to be sure the e-mail address is correct and try again."; } // end else - if(mysqli_num_rows($result) == 1) } // if (! $messages["errors"] )
The "from" part of the headers section is the sender's e-mail address, which has been stored in the config file, since it will be updated whenever the project is moved to a new server. The config_item() method retrieves it. The sender's e-mail address must be an account on the project web server, as the domain name of the sender e-mail must match the sending server's domain. Otherwise, network mail servers will reject the message as spam.
The special password reset link uses the URL for the project (site_url()), and the controller/method path for the "verify_link" method. The token is added as an additional URL segment, which will be passed to the verify_link method as an argument.
The headers section requires carriage returns to be inserted, and PHP does this with special /r/n characters (return-new line). They must be formatted in double quotes, or PHP renders them as literal characters.
Sending the e-mail is very simple. Just use the mail() function, using the arguments you have built upstream.
The last task is to send back the user messages:
(models/Auth_model/send_reset_link) ... if ((! $messages["errors"])&&($user_id > 0)){ $messages["status"] = 'success'; $messages["user_message"] = "A link to reset your password has been mailed to your e-mail address.
The link is valid for 1 hour."; }else{ $messages["status"] = 'failed'; } return $messages; } // -end- send_reset_link
The complete script for the models/Auth_model/send_reset_link is here:
(models/Auth_model/send_reset_link) public function send_reset_link($email){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["email_error"] = ""; $messages["user_message"] = ""; $user_id = 0; $email = trim($email); //check for an empty username if (strlen($email) < 1){ $messages["errors"] = true; $messages["email_error"] = 'You must enter your email address.'; } // if (strlen($email < 1) //check for valid email if (!filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = true; $messages["email_error"] = 'You must enter a valid email address.'; }else{ // if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = stripslashes($email); $email = strip_tags($email); $email = $this->db->escape_str($email); } // -end else- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) if(! $messages["errors"] ){ $sql = "SELECT id FROM users_hash WHERE email = '".$email."' "; $query = $this->db->query($sql); if($query->num_rows() == 1){ $row = $query->row_array(); $user_id = $row["id"]; $token = sha1($email.time()); $sql = "INSERT INTO `password_reset_log` (`id`, `user_id`, `reset_token`, `timestamp`) VALUES (NULL, '".$user_id."', '".$token."', NOW())"; $query = $this->db->query($sql); if($this->db->affected_rows() == 1){ //define the headers $to = $email; $from = config_item('from'); //is defined in the "config" file $subject = 'Password Reset Request'; $message_text = ' A password reset request has been made for your AMP JAM account that uses this e-mail address. If you did not initiate this request, please notify the security team at once. If you made the request, please click on the link below to reset your password. This link will expire one hour from the time this e-mail was sent. '.site_url().'/auth/verify_link/'.$token; //this should be a path to the Auth controller's verify_link method. The token is the 3rd URL segment //be sure the /r/n (carriage return) characters are in DOUBLE QUOTES! //PHP treats single quoted escaped characters differently, and things will break $headers = 'From: '.$from . "\r\n" . 'Reply-To: '.$from . "\r\n" . 'X-Mailer: PHP/' . phpversion(); mail($to, $subject, $message_text, $headers); }else{ $messages["errors"] = true; $messages["email_error"] = "There was a problem with the database. Your password cannot be reset."; } }else{ // else - if($this->db->affected_rows() == 1) $messages["email_error"] = "The e-mail address you entered was not found in the database.
Check to be sure the e-mail address is correct and try again."; } // end else - if(mysqli_num_rows($result) == 1) } // if (! $messages["errors"] ) if ((! $messages["errors"])&&($user_id > 0)){ $messages["status"] = 'success'; $messages["user_message"] = "A link to reset your password has been mailed to your e-mail address.
The link is valid for 1 hour."; }else{ $messages["status"] = 'failed'; } return $messages; } // -end- send_reset_link
Move the project to a server account and test it. Register an account with your e-mail address and go through the process to reset the password. You should be able to send yourself a reset message like the one shown here:
verify_link This step creates the feature that handles the user's click on the e-mail link. This script will:
Create a new method (function) in the Auth class (controllers/Auth.php) named "verify_link", and add an argument for the token ( verify_link($token) ).
Set up some data for the view in the $data array ( page title, etc.), then pass the token to the auth_model's verify_link method. The messages returned will include a user_id and a user_message. Move these to the $data array and load the view, passing it the data.
(controllers/Auth.php/verify_link) public function verify_link($token){ $data["page_title"] = "AMP JAMS | Lit RESET"; $data["this_page"] = "verify"; $data["token"] = $token; $messages = $this->auth_model->verify_link($token); $data["user_id"] = $messages["user_id"]; $data["user_message"] = $messages["user_message"]; $this->load->view('templates/head', $data); $this->load->view('auth/verify_link', $data); }
Next, open models/Auth_model.php and add a verify_link method in the Auth_model class. The first step is to set up the $messages variable and do some sanitizing for the $token.
(models/Auth_model.php/verify_link) public function verify_link($token){ $messages = array(); $messages["user_id"] = 0; $messages["user_message"] = ''; $user_id = 0; $user_message = ""; $token = trim($token); $token = stripslashes($token); $token = strip_tags($token); $token = $this->db->escape_str($token); ...
Note that the $token escape step is done with the db class's escape_str() method.
Continue by querying the password_reset_log and checking the elapsed time for the token.
(models/Auth_model.php/verify_link) ... $sql = "SELECT user_id, timestamp FROM password_reset_log WHERE reset_token = '".$token."' "; $query = $this->db->query($sql); if ($query->num_rows() == 1){ $row = $query->row_array(); $user_id = $row["user_id"]; $link_time = $row["timestamp"]; $sql = "SELECT TIMESTAMPDIFF(SECOND, '".$link_time."', NOW()) as time_elapsed"; $query = $this->db->query($sql); $row = $query->row_array(); if ($row["time_elapsed"] < 3600){ //1 hour $link_status = "valid"; }else{ //if ($row["time_elapsed"] > 3600) $link_status = "expired"; } // -end else- if ($row["time_elapsed"] > 3600) }else{ // if ($query->num_rows() == 1) $link_status = "no_record"; } // -end - if ($query->num_rows() == 1)
The user_id and timestamp details come from the password_reset_log record. To check the elapsed time, MySQL has a function called TIMESTAMPDIFF(), which requires:
It is used as part of a SELECT query, and the keyword "as" allows you to name the resulting "column" with a better name. We will use "time_elapsed".
The logic checks for an elapsed time of less than one hour (3600 seconds). Depending on whether the elaspsed time is within the time window, we will set a status variable to "valid", "expired", or other values that reflect the various states of failure.
Next, compose the user messages, depending on the status of the token. If the token is valid, build a string that draws a new password input form. Use a switch statement for the logic.
(models/Auth_model.php/verify_link) ... switch ($link_status){ case "valid": $user_message .=' <div id="user_message" class="success"> <p>Please enter a new password to use with your account.<br/>It must have at least 8 characters.</p> <form id="password_form" name="password_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-3"></div> <div class="col-6"> <div class="form-group"> <label for="password" class="control-label">Password </label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> <div id="password_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> <button type="submit" class="btn btn-primary float-right">Submit</button> </div> <!-- /.col-6 --> </div> <!-- /.row --> </form> </div> <!-- /.success --> '; break; case "expired": $user_message .=' <div class="error"> <p>The password reset link has expired.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; case "no_record": default: $user_message .=' <div class="error"> <p>The password reset token is not valid.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; }//switch $messages["user_id"] = $user_id; $messages["user_message"] = $user_message; return $messages; } //verify_link
Once the messages are composed, they are sent back to the controller. The controller will pass them along to the view.
Here is the complete script for the verify_link method in the model:
(models/Auth_model.php/verify_link) public function verify_link($token){ $messages = array(); $messages["user_id"] = 0; $messages["user_message"] = ''; $user_id = 0; $user_message = ""; $token = trim($token); $token = stripslashes($token); $token = strip_tags($token); $token = $this->db->escape_str($token); $sql = "SELECT user_id, timestamp FROM password_reset_log WHERE reset_token = '".$token."' "; $query = $this->db->query($sql); if ($query->num_rows() == 1){ $row = $query->row_array(); $user_id = $row["user_id"]; $link_time = $row["timestamp"]; $sql = "SELECT TIMESTAMPDIFF(SECOND, '".$link_time."', NOW()) as time_elapsed"; $query = $this->db->query($sql); $row = $query->row_array(); if ($row["time_elapsed"] < 3600){ //1 hour $link_status = "valid"; }else{ //if ($row["time_elapsed"] > 3600) $link_status = "expired"; } // -end else- if ($row["time_elapsed"] > 3600) }else{ // if ($query->num_rows() == 1) $link_status = "no_record"; } // -end - if ($query->num_rows() == 1) switch ($link_status){ case "valid": $user_message .=' <div id="user_message" class="success"> <p>Please enter a new password to use with your account.<br/>It must have at least 8 characters.</p> <form id="password_form" name="password_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-3"></div> <div class="col-6"> <div class="form-group"> <label for="password" class="control-label">Password </label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> <div id="password_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> <button type="submit" class="btn btn-primary float-right">Submit</button> </div> <!-- /.col-6 --> </div> <!-- /.row --> </form> </div> <!-- /.success --> '; break; case "expired": $user_message .=' <div class="error"> <p>The password reset link has expired.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; case "no_record": default: $user_message .=' <div class="error"> <p>The password reset token is not valid.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; }//switch $messages["user_id"] = $user_id; $messages["user_message"] = $user_message; return $messages; } //verify_link
Create a new view in the views/auth directory named "verify_link.php". First, define a few styles and the structure of the HTML markup:
(views/auth/verify_link.php) <style> .error { padding:30px;text-align:center;font-size:18px;font-weight:700;background-color:#ffffff;border:4px solid #990000; } .success{ text-align:left;font-size:18px;font-weight:400; } .success > p { text-align:center;margin-bottom:30px; } </style> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>AMP JAM Password Reset</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6 text-center mt-5"> <?= $user_message ?> </div> <!-- /col-6 --> </div> <!-- /row --> </div> <!-- /container-fluid --> </body>
Notice that the user message will be dropped in from the $data variable passed to the view. Remember, the variables have the name of the key used in the $data variable.
Follow this with the script for sending the data from the form back to the server via AJAX:
(views/auth/verify_link.php) ... <script type="text/javascript"> $(document).ready(function(){ // Attach a submit handler to the form $( "#password_form" ).submit(function( event ) { event.preventDefault(); $.post("<?=site_url()?>/auth/update_password", {id:<?= $user_id ?>, password:$("#password").val(), token:'<?= $token ?>'}, function(data){ //reset any previous error messages $("#password_error").html(""); $("#password_error").css("display","none"); if(data.status == "success"){ if(data.user_message > ' ' ){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.password_error > ' '){ $("#password_error").html(data.password_error); $("#password_error").css("display","block"); } } }, "json" ); //post }); //submit }); //ready </script> </html>
The form data will be sent to the update_password() method in the auth controller. The message that comes back will provide feedback about success and a button that navigates back to the log-in page.
Here is the completed script for the verify_link.php view:
(views/auth/verify_link.php) <style> .error { padding:30px;text-align:center;font-size:18px;font-weight:700;background-color:#ffffff;border:4px solid #990000; } .success{ text-align:left;font-size:18px;font-weight:400; } .success > p { text-align:center;margin-bottom:30px; } </style> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>AMP JAM Password Reset</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6 text-center mt-5"> <?= $user_message ?> </div> <!-- /col-6 --> </div> <!-- /row --> </div> <!-- /container-fluid --> </body> <script type="text/javascript"> $(document).ready(function(){ // Attach a submit handler to the form $( "#password_form" ).submit(function( event ) { event.preventDefault(); $.post("<?=site_url()?>/auth/update_password", {id:<?= $user_id ?>, password:$("#password").val(), token:'<?= $token ?>'}, function(data){ //reset any previous error messages $("#password_error").html(""); $("#password_error").css("display","none"); if(data.status == "success"){ if(data.user_message > ' ' ){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.password_error > ' '){ $("#password_error").html(data.password_error); $("#password_error").css("display","block"); } } }, "json" ); //post }); //submit }); //ready </script> </html>
If you have the project running on a webserver, you can test it by making a password reset request, sending yourself a reset e-mail, and clicking the link. If you are working localhost, make a password reset request, and get the token from your database table. In the browser, enter this URL:
<path-to-your-project>/index.php/auth/verify_link/<token>
You should see a form that looks like the screenshot above.
update_password The last piece of the password reset feature actually updates the password. The update_password feature needs to do the following things:
The data flow will be:
(controllers/Auth.php/update_password) public function update_password(){ $credentials = $this->input->post(); $messages = $this->auth_model->update_password($credentials); echo json_encode($messages); }
Next, add a new method to the models/Auth_model.php's Auth_model class. Start with setting up variables and validating the token and user_id:
(models/Auth_model.php/update_password) public function update_password($credentials){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["password_error"] = ""; $messages["user_message"] = ""; $user_id = intval($credentials["id"]); $token = $credentials["token"]; $validated = false; if($user_id < 1){ $messages["errors"] = true; $messages["password_error"] = 'Password cannot be reset'; } //revalidate the token and the user id if(! $messages["errors"]){ if($token > ''){ $sql="SELECT * from password_reset_log WHERE reset_token = '".$token."'"; $query = $this->db->query($sql); if ($query->num_rows() == 1){ $row = $query->row_array(); if($user_id == $row["user_id"]){ $validated = true; } } } //if($token > '') } //if(! $messages["errors"])
Next, check and encrypt the new password:
(models/Auth_model.php/update_password) ... if( $validated ){ $password = $credentials["password"]; trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = true; $messages["password_error"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = true; $messages["password_error"] = "Password encryption failed. You cannot reset your password at this time."; } //if($encrypted_password) } //if(strlen($password) < 8) } //if( $validated )
Now update the user record in the user_hash table:
(models/Auth_model.php/update_password) ... if( ! $messages["errors"] ){ $sql = "UPDATE users_hash set `password` = '".$password."' WHERE id = '".$user_id."' "; $query = $this->db->query($sql); if($this->db->affected_rows() == 1){ $this->session->set_userdata('user_id', $user_id); }else{ $messages["errors"] = true; $messages["password_error"] = 'There was a problem with the database.
Your password cannot be reset'; } // if($this->db->affected_rows() == 1) } // if ( ! $messages["errors"] )
Finally, compose any remaining user feedback messages and delete the password_reset_log record so the token can no longer be used:
(models/Auth_model.php/update_password) ... if ($this->session->userdata('user_id') > 0){ $messages["status"] = 'success'; $messages["user_message"] = '<p>Your password has been successfully reset.<br/><br/> <a href="'.site_url().'/auth"><button type="button" class="btn btn-primary">Log In</button></a></p>'; }else{ $messages["status"] = 'failed'; } // -end else- if ($_SESSION["user_id"] > 0) $sql = "DELETE from password_reset_log WHERE reset_token = '".$token."'"; $query = $this->db->query($sql); return $messages; } //update_password
The complete script for the update_password model script is below:
(models/Auth_model.php/update_password) public function update_password($credentials){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["password_error"] = ""; $messages["user_message"] = ""; $user_id = intval($credentials["id"]); $token = $credentials["token"]; $validated = false; if($user_id < 1){ $messages["errors"] = true; $messages["password_error"] = 'Password cannot be reset'; } //revalidate the token and the user id if(! $messages["errors"]){ if($token > ''){ $sql="SELECT * from password_reset_log WHERE reset_token = '".$token."'"; $query = $this->db->query($sql); if ($query->num_rows() == 1){ $row = $query->row_array(); if($user_id == $row["user_id"]){ $validated = true; } } } //if($token > '') } //if(! $messages["errors"]) if( $validated ){ $password = $credentials["password"]; trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = true; $messages["password_error"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = true; $messages["password_error"] = "Password encryption failed. You cannot reset your password at this time."; } //if($encrypted_password) } //if(strlen($password) < 8) } //if( $validated ) if( ! $messages["errors"] ){ $sql = "UPDATE users_hash set `password` = '".$password."' WHERE id = '".$user_id."' "; $query = $this->db->query($sql); if($this->db->affected_rows() == 1){ $this->session->set_userdata('user_id', $user_id); }else{ $messages["errors"] = true; $messages["password_error"] = 'There was a problem with the database.
Your password cannot be reset'; } // if($this->db->affected_rows() == 1) } // if ( ! $messages["errors"] ) if ($this->session->userdata('user_id') > 0){ $messages["status"] = 'success'; $messages["user_message"] = '<p>Your password has been successfully reset.<br/><br/> <a href="'.site_url().'/auth"><button type="button" class="btn btn-primary">Log In</button></a></p>'; }else{ $messages["status"] = 'failed'; } // -end else- if ($_SESSION["user_id"] > 0) $sql = "DELETE from password_reset_log WHERE reset_token = '".$token."'"; $query = $this->db->query($sql); return $messages; } //update_password
Test the project as described in the previous section, and enter a new passowrd in the form. If things are working, you should see the screen update with a success message, as shown in the screen shot here:
Lit RESET is complete! You have added an important feature to your application.
Here are completed versions of the scripts used for this project:
Controllers and Models
(controllers/Litjams.php)<?php defined('BASEPATH') OR exit('No direct script access allowed'); class Litjams extends CI_Controller { public function __construct() { parent::__construct(); $this->load->model('litlist_model'); $this->load->model('litform_model'); } public function index(){ $data["page_title"] = "AMP JAMS | Lit SITE"; $data["this_page"] = "home"; $data["row"] = $this->litlist_model->get_random_row(); $this->load->view('templates/head', $data); $this->load->view('index', $data); } public function litlist(){ $data["page_title"] = "AMP JAMS | Lit LIST"; $data["this_page"] = "litlist"; $data["records"] = $this->litlist_model->get_litlist_items(); $this->load->view('templates/head', $data); $this->load->view('litlist', $data); } public function detail($id){ $detail = $this->litlist_model->get_litlist_detail($id); $data["page_title"] = "AMP JAMS | ".$detail["item"]; $data["this_page"] = "litlist"; $data["row"] = $detail; $this->load->view('templates/head', $data); $this->load->view('detail', $data); } public function litform(){ $data["page_title"] = "AMP JAMS | Lit FORM"; $data["this_page"] = "contact"; $this->load->view('templates/head', $data); $this->load->view('litform', $data); } public function contact_post(){ $contact = $this->input->post(); $messages = $this->litform_model->contact_post($contact); echo json_encode($messages); } public function messages(){ $data["page_title"] = "AMP JAMS | Lit Messages"; $data["this_page"] = "messages"; $data["messages"] = $this->litform_model->get_messages(); $this->load->view('templates/head', $data); $this->load->view('messages', $data); } }
(models/Litlist_model.php)<?php defined('BASEPATH') OR exit('No direct script access allowed'); class Litlist_model extends CI_Model { public function __construct() { parent::__construct(); //this causes the database library to be autoloaded //loading of any other models would happen here } public function get_litlist_items(){ $sql = "SELECT * FROM `litlist`"; $query = $this->db->query($sql); return $query->result_array(); } public function get_litlist_detail($id){ $sql = "SELECT * FROM `litlist` WHERE id = '".$id."'"; $query = $this->db->query($sql); return $query->row_array(); } }
(models/Litform_model.php)<?php defined('BASEPATH') OR exit('No direct script access allowed'); class Litform_model extends CI_Model { public function __construct() { parent::__construct(); //this causes the database library to be autoloaded //loading of any other models would happen here } public function contact_post($contact){ $contact["name"] = $this->db->escape_str($contact["name"]); $contact["comment"] = $this->db->escape_str($contact["comment"]); $messages = array(); $messages["status"] = $this->db->insert('form_responses', $contact); if($messages["status"]){ $messages["success"] = '<p>Thank you for submitting your comment. <br/> <span class="mt-5 float-right"><i>The Web Site Team</i></span></p>'; }else{ $messages["failed"] = '<p>Sorry, but something went wrong. Please try again later. <br/> <span class="mt-5 float-right"><i>The Web Site Team</i></span></p>'; } return $messages; } }
(controllers/Auth.php)<?php defined('BASEPATH') OR exit('No direct script access allowed'); class Auth extends CI_Controller { public function __construct() { parent::__construct(); $this->load->model('auth_model'); } public function index(){ $data["page_title"] = "AMP JAMS | Lit LOGIN"; $data["this_page"] = "login"; $this->load->view('templates/head', $data); $this->load->view('auth/login', $data); } public function authenticate(){ $credentials = $this->input->post(); $messages = $this->auth_model->authenticate($credentials); echo json_encode($messages); } public function logout(){ $data["page_title"] = "AMP JAMS | Lit LOGOUT"; $data["this_page"] = "logout"; $this->session->user_id = 0; //delete the session $this->session->sess_destroy(); //delete the session $this->load->view('templates/head', $data); $this->load->view('auth/logout', $data); } public function register(){ $data["page_title"] = "AMP JAMS | Lit REGISTER"; $data["this_page"] = "register"; $this->load->view('templates/head', $data); $this->load->view('auth/register', $data); } public function new_account(){ $credentials = $this->input->post(); $messages = $this->auth_model->new_account($credentials); echo json_encode($messages); } public function send_reset_link(){ $email = $this->input->post('email'); $messages = $this->auth_model->send_reset_link($email); echo json_encode($messages); } public function verify_link($token){ $data["page_title"] = "AMP JAMS | Lit RESET"; $data["this_page"] = "verify"; $data["token"] = $token; $messages = $this->auth_model->verify_link($token); $data["user_id"] = $messages["user_id"]; $data["user_message"] = $messages["user_message"]; $this->load->view('templates/head', $data); $this->load->view('auth/verify_link', $data); } public function update_password(){ $credentials = $this->input->post(); $messages = $this->auth_model->update_password($credentials); echo json_encode($messages); } }
(models/Auth_model.php)<?php defined('BASEPATH') OR exit('No direct script access allowed'); class Auth_model extends CI_Model { public function __construct() { parent::__construct(); //this causes the database library to be autoloaded //loading of any other models would happen here } public function authenticate($credentials){ $messages = array(); $messages["status"] = 0; $messages["role"] = 0;; $messages["success"] = ""; $messages["failed"] = ""; $username = $credentials["username"]; $password = $credentials["password"]; $sql = "SELECT * FROM `users_hash` WHERE username = '".$username."' LIMIT 1"; $query = $this->db->query($sql); $row = $query->row_array(); if($row){ if(password_verify($password, $row["password"])){ $this->session->user_id = $row["id"]; $this->session->role = $row["role"]; } } if($this->session->user_id > 0){ $messages["status"] = "1"; $messages["role"] = $this->session->role; $messages["success"] = '<h3 class="text-center">You are now Logged In.</h3>'; }else{ $messages["failed"] = '<h3 class="text-center">The Log-in was not successful.</h3><div class="col-12 text-center"><a href="<?=site_url()?>/auth"><button type="button"class="btn btn-primary mt-5">Try Again</button></a></div>'; } return $messages; } public function new_account($credentials){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["username_length"] = ""; $messages["password_length"] = ""; $messages["username_exists"] = ""; $messages["email_exists"] = ""; $messages["email_validate"] = ""; $messages["success"] = ""; $messages["failed"] = ""; $username = $credentials["username"]; $email = $credentials["email"]; $password = $credentials["password"]; trim($username); //delete leading and trailing spaces if(strlen($username) < 5){ $messages["errors"] = 1; $messages["username_length"] = "The username must have at least 5 characters."; }else{ $username = $this->db->escape_str($username); } if (! filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = 1; $messages["email_validate"] = "There are problems with the e-mail address. Please correct them."; } trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = 1; $messages["password_length"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = 1; $messages["password_length"] = "Password encryption failed. You cannot register at this time"; } } if( ! $messages["errors"] ){ $sql = "SELECT * FROM `users_hash` WHERE username = '".$username."'"; $query = $this->db->query($sql); if($query->num_rows() > 0){ $messages["errors"] = 1; $messages["username_exists"] = "This username already exists. Please select another username."; } $sql = "SELECT * FROM `users_hash` WHERE email = '".$email."'"; $query = $this->db->query($sql); if($query->num_rows() > 0){ $messages["errors"] = 1; $messages["email_exists"] = "This email is already in use. You cannot register another account with this email address."; } } if( ! $messages["errors"] ){ $sql = "INSERT INTO `users_hash` (`id`, `username`, `email`, `password`, `role`) VALUES (NULL, '".$username."', '".$email."', '".$password."', '0')"; $query = $this->db->query($sql); $user_id = $this->db->insert_id(); if($user_id){ $this->session->user_id = $user_id; $this->session->role = 0; } // if($user_id) } // if( ! $messages["errors"] ) if($this->session->user_id > 0){ $messages["status"] = "1"; $messages["success"] = '<h3 class="text-center">You are now Registered and Logged In.</h3>'; }else{ $messages["failed"] = '<h3 class="text-center">The Registration was not successful.</h3><div class="col-12 text-center"><a href="'.site_url().'/auth/register" class="text-center"><button class="btn btn-primary mt-5">Try Again</button></a></div>'; } return $messages; } //new account public function send_reset_link($email){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["email_error"] = ""; $messages["user_message"] = ""; $user_id = 0; $email = trim($email); //check for an empty username if (strlen($email) < 1){ $messages["errors"] = true; $messages["email_error"] = 'You must enter your email address.'; } // if (strlen($email < 1) //check for valid email if (!filter_var($email, FILTER_VALIDATE_EMAIL)){ $messages["errors"] = true; $messages["email_error"] = 'You must enter a valid email address.'; }else{ // if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = stripslashes($email); $email = strip_tags($email); $email = $this->db->escape_str($email); } // -end else- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) if(! $messages["errors"] ){ $sql = "SELECT id FROM users_hash WHERE email = '".$email."' "; $query = $this->db->query($sql); if($query->num_rows() == 1){ $row = $query->row_array(); $user_id = $row["id"]; $token = sha1($email.time()); $sql = "INSERT INTO `password_reset_log` (`id`, `user_id`, `reset_token`, `timestamp`) VALUES (NULL, '".$user_id."', '".$token."', NOW())"; $query = $this->db->query($sql); if($this->db->affected_rows() == 1){ //define the headers $to = $email; $from = config_item('from'); //is defined in the "config" file $subject = 'Password Reset Request'; $message_text = ' A password reset request has been made for your AMP JAM account that uses this e-mail address. If you did not initiate this request, please notify the security team at once. If you made the request, please click on the link below to reset your password. This link will expire one hour from the time this e-mail was sent. '.site_url().'/auth/verify_link/'.$token; //this should be a path to the Auth controller's verify_link method. The token is the 3rd URL segment //be sure the /r/n (carriage return) characters are in DOUBLE QUOTES! //PHP treats single quoted escaped characters differently, and things will break $headers = 'From: '.$from . "\r\n" . 'Reply-To: '.$from . "\r\n" . 'X-Mailer: PHP/' . phpversion(); mail($to, $subject, $message_text, $headers); }else{ $messages["errors"] = true; $messages["email_error"] = "There was a problem with the database. Your password cannot be reset."; } }else{ // else - if($this->db->affected_rows() == 1) $messages["email_error"] = "The e-mail address you entered was not found in the database.
Check to be sure the e-mail address is correct and try again."; } // end else - if(mysqli_num_rows($result) == 1) } // if (! $messages["errors"] ) if ((! $messages["errors"])&&($user_id > 0)){ $messages["status"] = 'success'; $messages["user_message"] = "A link to reset your password has been mailed to your e-mail address.
The link is valid for 1 hour."; }else{ $messages["status"] = 'failed'; } return $messages; } // -end- send_reset_link public function verify_link($token){ $messages = array(); $messages["user_id"] = 0; $messages["user_message"] = ''; $user_id = 0; $user_message = ""; $token = trim($token); $token = stripslashes($token); $token = strip_tags($token); $token = $this->db->escape_str($token); $sql = "SELECT user_id, timestamp FROM password_reset_log WHERE reset_token = '".$token."' "; $query = $this->db->query($sql); if ($query->num_rows() == 1){ $row = $query->row_array(); $user_id = $row["user_id"]; $link_time = $row["timestamp"]; $sql = "SELECT TIMESTAMPDIFF(SECOND, '".$link_time."', NOW()) as time_elapsed"; $query = $this->db->query($sql); $row = $query->row_array(); if ($row["time_elapsed"] < 3600){ //1 hour $link_status = "valid"; }else{ //if ($row["time_elapsed"] > 3600) $link_status = "expired"; } // -end else- if ($row["time_elapsed"] > 3600) }else{ // if ($query->num_rows() == 1) $link_status = "no_record"; } // -end - if ($query->num_rows() == 1) switch ($link_status){ case "valid": $user_message .=' <div id="user_message" class="success"> <p>Please enter a new password to use with your account.<br/>It must have at least 8 characters.</p> <form id="password_form" name="password_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-3"></div> <div class="col-6"> <div class="form-group"> <label for="password" class="control-label">Password </label> <input type="password" id="password" name="password" class="form-control" placeholder="Password"> <div id="password_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> <button type="submit" class="btn btn-primary float-right">Submit</button> </div> <!-- /.col-6 --> </div> <!-- /.row --> </form> </div> <!-- /.success --> '; break; case "expired": $user_message .=' <div class="error"> <p>The password reset link has expired.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; case "no_record": default: $user_message .=' <div class="error"> <p>The password reset token is not valid.</p> <p>Your password cannot be reset using this link.</p> </div>'; break; }//switch $messages["user_id"] = $user_id; $messages["user_message"] = $user_message; return $messages; } //verify_link public function update_password($credentials){ $messages = array(); $messages["status"] = 0; $messages["errors"] = 0; $messages["password_error"] = ""; $messages["user_message"] = ""; $user_id = intval($credentials["id"]); $token = $credentials["token"]; $validated = false; if($user_id < 1){ $messages["errors"] = true; $messages["password_error"] = 'Password cannot be reset'; } //revalidate the token and the user id if(! $messages["errors"]){ if($token > ''){ $sql="SELECT * from password_reset_log WHERE reset_token = '".$token."'"; $query = $this->db->query($sql); if ($query->num_rows() == 1){ $row = $query->row_array(); if($user_id == $row["user_id"]){ $validated = true; } } } //if($token > '') } //if(! $messages["errors"]) if( $validated ){ $password = $credentials["password"]; trim($password); //delete leading and trailing spaces if(strlen($password) < 8){ $messages["errors"] = true; $messages["password_error"] = "The password must have at least 8 characters."; }else{ $encrypted_password = password_hash($password, PASSWORD_DEFAULT); if($encrypted_password){ $password = $encrypted_password; }else{ $messages["errors"] = true; $messages["password_error"] = "Password encryption failed. You cannot reset your password at this time."; } //if($encrypted_password) } //if(strlen($password) < 8) } //if( $validated ) if( ! $messages["errors"] ){ $sql = "UPDATE users_hash set `password` = '".$password."' WHERE id = '".$user_id."' "; $query = $this->db->query($sql); if($this->db->affected_rows() == 1){ $this->session->set_userdata('user_id', $user_id); }else{ $messages["errors"] = true; $messages["password_error"] = 'There was a problem with the database.
Your password cannot be reset'; } // if($this->db->affected_rows() == 1) } // if ( ! $messages["errors"] ) if ($this->session->userdata('user_id') > 0){ $messages["status"] = 'success'; $messages["user_message"] = '<p>Your password has been successfully reset.<br/><br/> <a href="'.site_url().'/auth"><button type="button" class="btn btn-primary">Log In</button></a></p>'; }else{ $messages["status"] = 'failed'; } // -end else- if ($_SESSION["user_id"] > 0) $sql = "DELETE from password_reset_log WHERE reset_token = '".$token."'"; $query = $this->db->query($sql); return $messages; } //update_password }
Views
(views/templates/head.php)<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"> <title><?=$page_title?></title> <link href="<?=base_url()?>assets/css/bootstrap.min.css" rel="stylesheet"> <script src="<?=base_url()?>assets/js/jquery-3.4.1.min.js" type="application/javascript"></script> <script src="<?=base_url()?>assets/js/bootstrap.min.js" type="application/javascript"></script> <script src="<?=base_url()?>assets/js/bootstrap.min.js.map" type="application/javascript"></script> <script> var this_page = "<?=$this_page?>"; var page_title = "<?=$page_title?>"; function navbar_update(this_page){ $("#"+this_page+"_item").addClass('active'); $("#"+this_page+"_link").append(' <span class="sr-only">(current)</span>'); } </script> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark" style="background-color:#ee4323;"> <a class="navbar-brand" href="<?=site_url()?>">AMP JAM LIT</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li id="home_item" class="nav-item"> <a id="home_link" class="nav-link" href="<?=site_url()?>">Home</a> </li> <li id="litlist_item" class="nav-item"> <a id="litlist_link" class="nav-link" href="<?=site_url()?>/litjams/litlist">The List</a> </li> <li id="contact_item" class="nav-item"> <a id="contact_link" class="nav-link" href="<?=site_url()?>/litjams/litform">Contact</a> </li> </ul> <ul id="right_navbar" class="navbar-nav ml-auto mr-5"> <?php if($this->session->user_id > 0){ if($this->session->role > 0){ echo ' <li id="messages_item" class="nav-item"> <a id="messages_link" class="nav-link" href="'.site_url().'/litjams/messages">Messages</a> </li>'; } echo ' <li id="logout_item" class="nav-item"> <a id="logout_link" class="nav-link" href="'.site_url().'/auth/logout">Log-Out</a> </li>'; }else{ echo ' <li id="register_item" class="nav-item"> <a id="register_link" class="nav-link" href="'.site_url().'/auth/register">Register</a> </li> <li id="login_item" class="nav-item"> <a id="login_link" class="nav-link" href="'.site_url().'/auth">Log-In</a> </li>'; } ?> </ul>
(views/index.php)<div class="container-fluid"> <div id="headline" class="row mt-3"> <div class="col-12 text-center"> <h1>Lit JAM Site</h1> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div id="subtitle" class="col-12 text-center"> <h3>The Top-Ten List</h3> </div> <!-- /col-12 --> </div> <!-- /row --> <div id="content" class="row"> <div class="col-2"></div><!-- spacer --> <div class="col-2 mt-5"> <!-- navigation --> <a href="<?=site_url()?>/litjams/litlist" ><h4>Top Ten List</h4></a> <a href="<?=site_url()?>/litjams/litform" ><h4>Contact Us</h4></a> </div> <div class="col-3 text-center"> <!-- image --> <a href="<?=site_url()?>/litjams/detail/<?=$row["id"]?>"> <img src="<?=base_url()?>assets/images/<?=$row["image"]?>" width="100%"; /> </a> </div> <!-- /image --> <div class="col-5"> <!-- caption --> <a href="<?=site_url()?>/litjams/detail/<?=$row["id"]?>"> <h2 style="margin-top:200px;"><?=$row["item"]?></h2> </a> </div> </div> <!-- /row --> </div> <!-- /container-fluid --> </body> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); }); //ready </script> </html>
(views/litlist.php)<style> .cursor-pointer {cursor:pointer;} </style> <div class="container-fluid"> <div id-"headline" class="row mt-5"> <div class="col-12 text-center"> <h2>LIT Top 10 List</h2> </div> <!-- /.col-12 --> </div> <!-- /.row --> <?php foreach ($records as $record){ echo ' <div class="row record-item mt-5 cursor-pointer" data-id="'.$record["id"].'" data-item="'.$record["item"].'"> <div class="col-1"></div> <!-- spacer --> <div class="col-3"><img src="'.base_url().'assets/images/'.$record["image"].'" width="100%"/></div> <div class="col-7"><b>'.$record["item"].'</b> '.$record["description"].'</div> </div> <!-- /.row -->'; } //foreach ?> </div> <!-- /.container-fluid --> </body> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $(".record-item").on("click", function(){ var id = $(this).data('id'); show_detail(id); }); //on() }); //document.ready function show_detail(id){ window.location.assign("<?=site_url()?>/litjams/detail/"+id); } </script> </html>
(views/detail.php)<div class="container-fluid"> <div id-"headline" class="row mt-5"> <div class="col-12 text-center"> <?php if($row){ echo '<h1>'.$row["item"].'</h1>'; }else{ echo '<h2>There has been a database error.</h2>'; } ?> </div> <!-- /.col-12 --> </div> <!-- /.row --> <?php if($row){ echo ' <div class="row mt-3"> <div class="col-1"></div> <!-- spacer --> <div class="col-4"><img src="'.base_url().'assets/images/'.$row["image"].'" width="100%"/></div> <div class="col-6">'.$row["description"].'</div> </div> <!-- /.row -->'; } ?> <div class="row mt-4 mb-5"> <div class="col-12 text-center"> <a href="<?=site_url()?>/litjams/litlist"><button class="btn btn-primary">Back to The List</button></a> </div> <!-- /.col-12 --> </div> <!-- /.row --> </div> <!-- /.container-fluid --> </body> </html>
(views/litform.php)<div class="container-fluid"> <div id="headline" class="row mt-5"> <div class="col-12 text-center"> <h2>Lit JAMS Contact Form</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="contact_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> Name: <input type="text" id="name" name="name" class="form-control" value="" placeholder="Enter Name" required/><br/> E-mail: <input type="email" id="email" name="email" class="form-control" value="" placeholder="Enter E-mail" required/><br/> Comment: <textarea id="comment" name="comment" class="form-control" value="" placeholder="Add your comment here:"></textarea><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> </body> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#contact_form").submit(function(event){ event.preventDefault(); $.post("<?=site_url()?>/litjams/contact_post", $("#contact_form").serialize(), function(data){ //handle messages here if(data.status){ $("#form-container").html(data.success); }else{ $("#form-container").html(data.failed); } }, "json" ); //post }); //submit }); //document.ready </script> </html>
(views/messages.php)<div class="container-fluid"> <div id="headline" class="row mt-5"> <div class="col-12 text-center"> <?php if(count($messages) > 0){ echo '<h1>Contact Form Messages</h1>'; }else{ echo '<h2>There are no messages at this time.</h2>'; } ?> </div> <!-- /.col-12 --> </div> <!-- /.row --> <?php if($this->session->role > 0){ foreach($messages as $message){ echo' <div class="row mt-3"> <div class="col-2"></div> <!-- spacer --> <div class="col-2">'.$message["name"].'<br/> <a href="mailto:'.$message["email"].'">'.$message["email"].'</a><br/> ['.$message["timestamp"].'] </div> <div class="col-6">'.$message["comment"].'</div> </div> <!-- /.row -->'; } //foreach }else{ echo' <div class="row mt-3"> <div class="col-12 text-center"><h3>You are not authorized to view the messages.</h3></div> </div> <!-- /.row -->'; } //else if ?> </div> <!-- /.container-fluid --> </body> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); }); //document.ready </script> </html>
Auth Views
(views/auth/login.php)<div class="container-fluid"> <div id="headline" class="row mt-5"> <div class="col-12 text-center"> <h2>Full Stack Amp Jam Log-in</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="login_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/><br/> Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/><br/> <button type="submit" id="submit" class="btn btn-primary float-right">Submit</button> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#login_form").submit(function(event){ event.preventDefault(); $.post("<?=site_url()?>/auth/authenticate", $("#login_form").serialize(), function(data){ //handle messages here if(data.status){ $("#form-container").html(data.success); right_navbar_update(data.role); }else{ $("#form-container").html(data.failed); } }, "json" ); //post }); //submit }); //document.ready function right_navbar_update(role){ var html = ""; if (role > 0) { html = '<li id="messages_item" class="nav-item">'+ '<a id="messages_link" class="nav-link" href="<?=site_url()?>/litjams/messages">Messages</a>'+ '</li>'; } html += '<li id="logout_item" class="nav-item">'+ '<a id="logout_link" class="nav-link" href="<?=site_url()?>/auth/logout">Log-Out</a>'+ '</li>'; $("#right_navbar").html(html); } </script> <!-- -------------------------- AMP JAM RESET Reset Password Modal ------------------------- --> <div class="modal fade" id="forgotModal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">AMP JAM RESET Reset Password</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <!-- /.modal-header --> <div class="modal-body"> <form id="reset_form" name="reset_form" class="form-horizontal" method="" action="" > <div class="row"> <div class="col-12"> <div class="row" style="padding:2em;"> <div class="form-group"> <label for="email" class="control-label">Enter your E-mail:</label> <input type="text" id="email" name="email" class="form-control" placeholder="E-mail address"> <div id="email_error" style="display:none;color:#990000;"></div> </div> <!-- /.form-group --> </div> <!-- /.row --> <div class="row row-gap"> <div class="col-11"> <button type="submit" class="btn btn-primary float-right">Reset Password</button> <div id="user_message" style="display:none;color:#990000;"></div> </div> <!-- /.col-11 --> </div> <!-- /.row row-gap --> </div> <!-- /col-12 --> </div> <!-- /.row --> </form> </div> <!-- /.modal-body --> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> <!-- /.modal-footer --> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> <!-- -------------------------- end Reset Password Modal ------------------------------ --> <script type="text/javascript"> // Attach a submit handler to the form $( "#reset_form" ).submit(function( event ) { event.preventDefault(); $.post("<?=site_url()?>/auth/send_reset_link", {email:$("#email").val()}, function(data){ //reset the error messages $("#user_message").html(""); $("#user_message").css("display","none"); $("#email_error").html(""); $("#email_error").css("display","none"); if(data.status == "success"){ if(data.user_message != null){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.email_error != null){ $("#email_error").html(data.email_error); $("#email_error").css("display","block"); } } }, "json" ); //post });//submit </script> </body> </html>
(views/auth/logout.php)<div class="container-fluid"> <div id="headline" class="row mt-5"> <div class="col-12 text-center"> <h2>Amp Jam Lit Log-out</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div class="col-4 text-center"> <h3>You are now Logged Out.</h3> <a href="<?=site_url()?>/auth"><button class="btn btn-primary mt-5">Log In</button></a> </div> <!-- /.col-4 --> </div> <!-- /.row --> </div> <!-- /.container-fluid --> </body> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); }); //document.ready </script> </html>
(views/auth/register.php)<div class="container-fluid"> <div id="headline" class="row mt-5"> <div class="col-12 text-center"> <h2>Amp Jam Lit Register</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <form id="register_form" method="POST" action=""> <div class="row mt-5"> <div class="col-4"></div> <!-- spacer --> <div id="form-container" class="col-4"> <div>User Name: <input type="text" id="username" name="username" class="form-control" value="" placeholder="Enter User Name" required/></div> <div id="username_length" class="text-danger"></div> <div id="username_exists" class="text-danger"></div> <div class="mt-3">E-mail: <input type="email" id="email" name="email" class="form-control" value="" placeholder="Enter E-mail" required/></div> <div id="email_exists" class="text-danger"></div> <div id="email_validate" class="text-danger"></div> <div class="mt-3">Password: <input type="password" id="password" name="password" class="form-control" value="" placeholder="Enter Password" required/></div> <div id="password_length" class="text-danger"></div> <div class="mt-5"><button type="submit" id="submit" class="btn btn-primary float-right">Submit</button></div> </div> <!-- /#form-container --> </div> <!-- /.row --> </form> </body> <script> $(document).ready(function(){ document.title = page_title; navbar_update(this_page); $("#register_form").submit(function(event){ event.preventDefault(); $.post( "<?=site_url()?>/auth/new_account", $("#register_form").serialize(), function(data){ if(data.status){ $("#form-container").html(data.success); right_navbar_update(); }else{ if(data.errors){ // handle error messages here for (var key in data){ switch(key){ case "status": case "errors": case "success": case "failed": break; default: $("#"+key).html(data[key]); $("#"+key).css("display","block"); break; } //switch } //for-in }else{ $("#form-container").html(data.failed); //registration failed, but without errors } //if data.errors } //if data.status }, //callback function "json" ); //post }); //submit }); //document.ready function right_navbar_update(){ var html = '<li id="logout_item" class="nav-item">'+ '<a id="logout_link" class="nav-link" href="<?=site_url()?>/auth/logout">Log-Out</a>'+ '</li>'; $("#right_navbar").html(html); } </script> </html>
(views/auth/verify_link.php)<style> .error { padding:30px;text-align:center;font-size:18px;font-weight:700;background-color:#ffffff;border:4px solid #990000; } .success{ text-align:left;font-size:18px;font-weight:400; } .success > p { text-align:center;margin-bottom:30px; } </style> <div class="container-fluid"> <div class="row"> <div class="col-12 text-center mt-5"> <h2>AMP JAM Password Reset</h2> </div> <!-- /col-12 --> </div> <!-- /row --> <div class="row"> <div class="col-3"></div> <!-- spacer --> <div class="col-6 text-center mt-5"> <?= $user_message ?> </div> <!-- /col-6 --> </div> <!-- /row --> </div> <!-- /container-fluid --> </body> <script type="text/javascript"> $(document).ready(function(){ // Attach a submit handler to the form $( "#password_form" ).submit(function( event ) { event.preventDefault(); $.post("<?=site_url()?>/auth/update_password", {id:<?= $user_id ?>, password:$("#password").val(), token:'<?= $token ?>'}, function(data){ //reset any previous error messages $("#password_error").html(""); $("#password_error").css("display","none"); if(data.status == "success"){ if(data.user_message > ' ' ){ $("#user_message").html(data.user_message); $("#user_message").css("display","block"); } }else{ if(data.password_error > ' '){ $("#password_error").html(data.password_error); $("#password_error").css("display","block"); } } }, "json" ); //post }); //submit }); //ready </script> </html>
You can open a completed version of the project in your browser here.
If you want to download completed versions of the PHP scripts and image files, you can find them here.