It seems like you are using an ad blocker. To enhance your experience and support our website, please consider:

  1. Signing in to disable ads on our site. Our users don't see ads
  2. Or, Sign up if you don't have an account yet
  3. Or, disable your adblocker by doing this:
    • Click on the adblocker icon in your browser's toolbar.
    • Select "Pause on this site" or a similar option for lancecourse.com.

How to add Sign Up, Sign In and User Dashboard to a web site?

By zooboole

Hi, this is zooboole again, welcome to this new tutorial on how to implement your own membership system on a website. In this tutorial, I am going to show you how you can add the sign up, sign in, and the user dashboard modules to your website. Believe me, it's easy...

Sign up

The scope: when do you need this?

  1. When you have a website that has to manage a community of users, or clients, or some other type of users who have to come back to the site over and over again.

  2. When you want to restrict the access to some parts or options of a website or an application.

    Actually, on lancecourse.com you should have noticed that there are some things you can't do unless you are a member (like comment tutorials). There are some other options (Like download source codes, post in the forum) that you can't access unless you are logged in. And the last thing to notice too is that if you are not a premium member you can't download pdf format of tutorials, a zip file of sources codes, access courses, live support, etc.

    So, the point is to have some sort of community around a website that has access to some parts of a website or an application. This can be implemented on games websites, e-commerce websites, forum or chat websites, etc

    Since every user should be identified as unique with a username and a password, we need to allow them to provide these details to our website. The website will then store it. When they come back another time we can ask them to provide their username and password to identify themselves. Once they provide their details, we can validate it against the information stored in our database.

Specifications: Structure of the project

It looks like till here we all are OK with the mission. I want to build something that can be easily plugged into your existing project. I don't want you to break the structure of your HTML layout neither the structure of your PHP code. For this reason, I will be using Oriented Object Programming and a very simple folder structure. I will not use any MVC framework just for simplicity even though that would have been cleaner.

I only want to focus on how such system works. So there is no need to worry myself about how to use a framework etc.

To make it easier I will use the similar structure of folder to the one I used in the news site project:

  • Membership/
    • classes/
      • model.class.php
      • session.php
    • config/
      • dbconnect.php
    • includes/
      • header.php
      • footer.php
      • log-user-in.php
      • sign-user-up.php
    • design/
      • style.css
      • materialize
    • index.php
    • signup.php
    • dashboard.php
    • logout.php

Before I explain what each file does and what code is in it, have a look at the following image. The image illustrates somehow how the files and folders are connected to each other:

Files structure

Since I didn't want to use MVC Design pattern and I wanted to use Oriented Object I had to have a simple structure of folders. It should be easily manipulated. We should be able to reuse some of other projects without having to embark the whole project.

Data model

Create a database and name it membership or any other name that you can easily remember or you like. Remember that no matter the name you use, you should go in the config/dbconnect.php to set everything up there.

Then create a table and name it users or a name of your choice.

--
-- Table structure for table `users`
--

CREATE TABLE IF NOT EXISTS `users` (
  `userid` int(11) NOT NULL AUTO_INCREMENT,
  `user_firstname` varchar(70) NOT NULL,
  `user_lastname` varchar(120) NOT NULL,
  `user_email` varchar(140) NOT NULL,
  `user_password` varchar(80) NOT NULL,
  `date_signed_up` varchar(36) NOT NULL,
  PRIMARY KEY (`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

What does each file do?

  • includes/header.php

    This file defines the basic HTML structure for our pages. includes/header.php brings our CSS files at the top of our pages. It also brings the session class and initiates it to make the session available on all pages.

    <?php 
        require_once __DIR__. '/../classes/session.php';
        require __DIR__. '/../config/dbconnect.php'; // database connection
        require __DIR__. '/../classes/model.class.php'; // Model
    
        $Dbhandler  = new Config() ;
        $Model      = new Model($Dbhandler);
    
         !isset($_SESSION) ? session::init() : null ;
    
         // automatic connection system
         //if a cookie is set and the user is not actually connected
         if( isset($_COOKIE['membership']) && !isset($_SESSION['user_session']) )
         {
        // Get the user's email from the cookie
        $cookie = explode("#~#",$_COOKIE['membership']);
        $cookie_email = isset($cookie[0]) ? $cookie[0] : null;
        $cookie_hash = isset($cookie[1]) ? $cookie[1] : null;
    
        $userData = $Model->getUser( $cookie_email ) ;
    
        // We create a hash of the user email and his IP address like we did while creating the cookie with our hashing function
        $input = $cookie_email.$_SERVER['REMOTE_ADDR'];
    
        if( $cookie_hash === crypt($input, $cookie_hash) ){
            // we use the email from our data base to set the session, not the one from the cookie
            $_SESSION['user_session'] = $userData->user_email;
    
            // we set the cookie again to extend its expiration date
            setcookie('membership', $cookie_email.'#~#'.$crypt, time() + 3600 * 24 * 7, '/', '', false, true);
        }else{
            // Otherwise, we unset the cookie by setting its expiration date to NOW
            setcookie('membership', '', time() - 3600, '/', '', false, true);
            }
        }
    ?>
      <html lang="en">
          <head>
             <title>Welcome to Membership</title>
              <!--Import materialize.css-->
               <link type="text/css" rel="stylesheet" href="./design/materialize/css/materialize.min.css"  media="screen,projection"/>
    
           <!--Let browser know website is optimized for mobile-->
            <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
            <!-- Our customized css -->
            <link rel="stylesheet" type="text/css" href="./design/style.css">
          </head>
          <body>
  • includes/footer.php

    The includes/footer.php closes the body of our document with </body></html>. It also brings in any necessary javascript files before closing the document.

        </div><!--  Closing main container-->
    </body>
      <!-- Insert javascripts here if available -->
      <!--Import jQuery before materialize.js-->
      <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
      <script type="text/javascript" src="./design/materialize/js/materialize.min.js"></script>
    </html>

    So, like it shows on the image above, the header.php and footer.php are like hat and shoes for each one of our files. They are feeding each page with the HTML skeleton, CSS, and javascript.

Note: I use Materialized as CSS framework. It's just like Twitter Bootstrap or Foundation. Read more about it here

  • config/dbconnect.php

`

<?php

class Config extends PDO{

        public $db;
        private $db_engine = 'mysql';
        private $db_host = 'localhost';

        private $db_user = 'username';
        private $db_password = 'password';
        private $db_base = 'membership';

        function __construct()
        {
            try{
                //setter: sets the database handler
                $this->db = new PDO("".$this->db_engine.":host=$this->db_host; dbname=$this->db_base", $this->db_user,$this->db_password);
            }  
            catch (PDOException $e){
                $e->getMessage();
                 // die(json_encode(array('outcome' => false, 'message' => 'Unable to connect')));
            }
        }

}

This file contains a class which constructor returns a PDO object that is used to query our database.

  • classes/model.class.php

`

<?php

    /*
     The model class will be used to create functions that can help us interact with our database
    */

    class Model 
    {

        private $errors = array();

        public function __construct( $dbh )
        {
            $this->db = $dbh->db;
        }

        /**
        */
        public function validateForm( $data )
        {

            // Verify if all fields are filled
            foreach ($data as $index => $value) {
                if ( empty($value) || trim($value) == '' ) 
                {
                    $this->errors['error_'.$index] =  'Please fill the '.$index.' field.';
                }
            }
            // We return false if there is no error, otherwise we return the errors
            return sizeof($this->errors) == 0 ? false : $this->errors;

        }

        public function hash_password( $password )
        {
            if (!empty($password)) {
                $jeton = "";
                $salt_chars = array_merge(range('A','Z'), range('a','z'), range(0,9));
                for($i = 0; $i < 22; $i++) {
                  $jeton .= $salt_chars[array_rand($salt_chars)];
                }
                return crypt($password, sprintf('$2y$%02d$', 7) . $jeton);

            }else{
                return false;
            }
        }

        public function signIn( $email, $password )
        {
            $query = $this->db->prepare( " SELECT user_email, user_password 
                                        FROM users 
                                        WHERE user_email = ?
                                    " );
            if ($query->execute(array($email)))
            {
                $query->setFetchMode(PDO::FETCH_OBJ);
                $fetchedData = $query->fetch();
                $digest = isset($fetchedData->user_password) ? $fetchedData->user_password : null;
                if (!is_null($digest)) {
                    return $digest === crypt($password, $digest);
                }else{
                    return false;
                }
            }else{
                return false;
            }

        }

        public function emailAlreadyUsed( $email )
        {
            $query = $this->db->prepare("SELECT COUNT(*) AS num FROM users WHERE user_email = ?");
            if ($query->execute(array($email)))
            {
                $query->setFetchMode(PDO::FETCH_OBJ);
                $fetchedData = $query->fetchAll();

                $total = isset($fetchedData[0]) ? $fetchedData[0]->num : null;
                return $total != 0;
            }else{
                return false;
            }
        }

        public function addUser($email, $password)
        {
            if (!empty($email) && !empty($password)) {
                $query = $this->db->prepare("INSERT INTO users (user_email, user_password, date_signed_up)  VALUES(?,?,?) ");
                return $query->execute(array($email, $this->hash_password($password), time()));
            }else{
                return false;
            }
        }

    }

The model class contains some functions that are common to our sign up and sign in forms. By putting them together in one class, it avoids us to repeat same functions many times.

  • classes/session.php

`

<?php
class session
{

    /*
     * Start the session
     *
     */
    public static function init ()
    {
        @session_start();
    }

    /*
     * Set session key
     *
     */
    public static function set ($key,$value)
    {
        $_SESSION[$key] = $value;
        return true;
    }

    /*
     * Get a session from the key
     *
     */
    public static function get ($key)
    {
       return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
    }

    /*
     * Unsets a session through the key
     *
     */
    public static function destroy ($key)
    {
        unset($_SESSION[$key]);
        //session_destroy();
    }

}

The session class contains many static functions that can be called at any point of our application.

  • Index.php

    The index.php file is the first file loaded when a user comes on the site. So I have placed the user logging in form there. Because the user will often do that action. By placing it just at the start of the site, it becomes easier for our users to log in.

    On the same page, the user has the possibility to go to the sign-up page and create his account if he is new.

    The index.php includes the /includes/header.php and /includes/footer.php to benefit from the HTML header, the css, javascript and the session.

`

    <?php 
    require __DIR__. '../includes/header.php'; 
    require __DIR__. '../includes/log-user-in.php'; //kind of controller for the login
    isset($_SESSION['user_session']) ? header("location:dashboard.php"):null;
?>

<div class="row">

    <div class="col s10 offset-s1 m11 l6 z-depth-1 white">
        <div class="center-align">
            <i class="mdi-action-lock-open prefix adlaptop"></i>
            <div class="divider"></div>
            <h2>Sanctuary</h2>
            <p>Provide your secret information to enter the lodge!</p>
            <p>~ <b>Love coding</b> ~</p>
        </div>
    </div>

    <div class="col s10 offset-s1 m11 l6 teal lighten-5">
        <div class="center-align">
            <h3>Already member?</h3>
            <p>Sign in to access your dashborad</p>
        </div>

        <form class="col s12" action="<?= $_SERVER['PHP_SELF'] ?>" method="post">
            <div class="row">
                <span class="red lighten-5"><?= isset($_SESSION['login_issue']) && !empty($_SESSION['login_issue']) ? $_SESSION['login_issue']: ''?></span>
                    <?php session::destroy('login_issue') ?>

                <div class="input-field col s10 offset-s1">
                    <input type="email" name="email" id="email" class="validate">
                    <label for="email">Your email</label>
                    <span class="red lighten-5"><?= isset($_SESSION['error_email']) && !empty($_SESSION['error_email']) ? $_SESSION['error_email']: ''?></span>
                    <?php session::destroy('error_email') ?>
                </div>
                <div class="input-field col s10 offset-s1">
                    <input type="password" name="password" id="password" class="validate">
                    <label for="password">Your password</label>
                    <span class="red lighten-5"><?= isset($_SESSION['error_password']) && !empty($_SESSION['error_password']) ? $_SESSION['error_password']: ''?></span>
                    <?php session::destroy('error_password') ?>
                </div>

                <div class="row">
                    <div class="col s5 offset-s1 right-align">
                        <input type="checkbox" class="with-gap" name="persist" id="persist"  />
                        <label for="persist">Remember me</label>
                        </div>
                    <div class="col s5 offset-s1 left-align">
                        <a href="signup.php">New? Sign up here</a>
                    </div>
                </div>

                <div class="center-align">
                    <button class="waves-effect waves-light btn">Sign in</button>
                </div>
            </div>
        </form>
    </div>

</div>

<?php require DIR. '../includes/footer.php'; ?>

The index.php has two main parts, the PHP part, located a the beginning of the file and the rest is some HTML code which defines the logging in form.

What you need to understand well is the PHP part:

<?php 
        require __DIR__. '../includes/header.php'; 
        require __DIR__. '../includes/log-user-in.php'; //kind of controller for the login
        isset($_SESSION['user_session']) ? header("location:dashboard.php"):null;
    ?>

In this code, the first line includes the header.php mentioned above. The second line includes another file named includes/log-user-in.php. This file is in charge of receiving directly the user's actions: like when he clicks on "Sign in" button. The page will verify if the form is submitted and take some other actions. The last line always checks if the user is already logged in. If that's the case the user will be redirected automatically to his dashboard.

There is no need for a logged user to see the logging in form again. If that happens, the user may try to log in again.

  • signup.php

<?php require DIR. '../includes/header.php'; require DIR. '../includes/sign-user-up.php'; //kind of controller for the login isset($_SESSION['user_session']) ? header("location:dashboard.php"):null; ?>

<div class="row">

    <div class="col s10 offset-s1 m11 l6 z-depth-1 white">
        <div class="center-align">
            <i class="mdi-hardware-laptop-mac prefix adlaptop"></i>
            <div class="divider"></div>
            <h2>Geek Lodge</h2>
            <p>Noone enters if he isn't a coder. Code is our white cells.</p>
            <p>~ <b>Love coding</b> ~</p>
        </div>
    </div>

    <div class="col s10 offset-s1 m11 l6 teal lighten-5">
        <div class="center-align">
            <h3>Sign up</h3>
            <p>Create your account and become member</p>
        </div>

        <form class="col s12" action="<?= $_SERVER['PHP_SELF'] ?>" method="post">
            <div class="row">

                <span class="red lighten-5"><?= isset($_SESSION['error_general']) && !empty($_SESSION['error_general']) ? $_SESSION['error_general']: ''?></span>
                    <?php session::destroy('error_general') ?>

                    <span class="green lighten-2"><?= isset($_SESSION['confirmation']) && !empty($_SESSION['confirmation']) ? $_SESSION['confirmation']: ''?></span>
                    <?php session::destroy('confirmation') ?>

                <div class="input-field col s10 offset-s1">
                    <input type="email" name="email" id="email" class="validate">
                    <label for="email">Your email</label>

                    <span class="red lighten-5"><?= isset($_SESSION['error_email']) && !empty($_SESSION['error_email']) ? $_SESSION['error_email']: ''?></span>
                    <?php session::destroy('error_email') ?>
                </div>

                <div class="input-field col s10 offset-s1">
                    <input type="password" name="password" id="password" class="validate">
                    <label for="password">Your password</label>

                    <span class="red lighten-5"><?= isset($_SESSION['error_password']) && !empty($_SESSION['error_password']) ? $_SESSION['error_password']: ''?></span>
                    <?php session::destroy('error_password') ?>
                </div>

                <div class="input-field col s10 offset-s1">
                    <input type="password" name="repassword" id="repassword" class="validate">
                    <label for="repassword">Re-enter password</label>

                    <span class="red lighten-5"><?= isset($_SESSION['error_repassword']) && !empty($_SESSION['error_repassword']) ? $_SESSION['error_repassword']: ''?></span>
                    <?php session::destroy('error_repassword') ?>
                </div>

                <div class="row">
                    <div class="col s11 offset-s1 center-align">
                        <a href="index.php">Already member? Sign in here</a>
                    </div>
                </div>

                <div class="center-align">
                    <button class="waves-effect waves-light btn">Create account</button>
                </div>
            </div>
        </form>
    </div>

</div>

<?php require DIR. '../includes/footer.php'; ?>

The signup.php file is exactly structured like the index.php. They all include the header.php, footer.php, and their processing page. In the case of signup.php, the processing page is named includes/sign-user-up.php.

  • includes/log-user-in.php

`

<?php
require __DIR__. '/../config/dbconnect.php'; // database connection
require __DIR__. '/../classes/model.class.php'; // Model

$Dbhandler  = new Config() ;
$Model      = new Model($Dbhandler);

if ( isset($_POST) && !empty($_POST) ) 
{

    if ( !$return = $Model->validateForm($_POST)  ) 
    {
        $email      =  filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
        $password   =  $_POST['password'];

        if ($Model->signIn( $email, $password )) 
        {   
            session::set('user_session', $email);
        }else{
            session::set('login_issue', "Can't log you in. check your details.");
        }

    }else{
        foreach ($return as $field => $error) {
            session::set($field, $error);
        }
    }

}

This file captures all actions the user may take in the index.php file. Actually, the file is little because the project is small. We have only one action, which is to verify if a form is submitted.

Since this file is included in the index.php and the index.php file has a form. The action of the form is in the page itself:

<form class="col s12" action="<?= $_SERVER['PHP_SELF'] ?>" method="post">

So, when the user clicks on Sign In, the following action will be triggered:

if ( isset($_POST) && !empty($_POST) ) 
{

if the form is submitted we call a little function named validateForm() which checks if all fields in the form are filled:

if ( !$return = $Model->validateForm($_POST)  ) 
    {

if the form is not well filled the function returns an array() of errors. When the form is well filled, we store what the user just typed in variables ($email and $password), then we call another function named signIn(). This function checks if the user email and password match one and a single record in our database. if that's the case the user session is created.

Since we remain in index.php, the following line is expecting that session:

isset($_SESSION['user_session']) ? header("location:dashboard.php"):null;

Once the session is set, this code will automatically redirect the user to the dashboard.

Now you may be wondering where are the functions validateForm() and signIn() coming from. And what's the meaning of this code:

$Dbhandler  = new Config() ;
$Model      = new Model($Dbhandler);

Before answering to this, notice the beginning of the includes/log-user-in.php file. It includes two other files: config/dbconnect.php and classes/model.class.php. These two files have classes declared inside. The class in config/dbconnect.php is in charge of connecting our application to a database using PDO. So it returns a PDO object that can be used to query our database. in order to get a PDO object that we can use for that matter we do this:

$Dbhandler  = new Config() ;

The $Dbhandler now holds the PDO object. This is called instantiate a class. read more about it here.

The same thing is done with the class in classes/model.class.php.


  • includes/sign-user-up.php

`

<?php
    require __DIR__. '/../config/dbconnect.php'; // database connection
    require __DIR__. '/../classes/model.class.php'; // Model

    $dbhandler = new Config() ;
    $Model = new Model($dbhandler);

    if ( isset($_POST) && !empty($_POST) ) 
    {

        if ( !$return = $Model->validateForm($_POST)  ) 
        {
            if ($_POST['password'] === $_POST['repassword']) {

                $email      =  filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
                $password   =  $_POST['password'];

                if (!$Model->emailAlreadyUsed($email)) 
                {
                    if ($Model->addUser($email, $password)) 
                    {
                        session::set('confirmation', 'You have successfully signed up!');
                        // If you want to can send a confirmation email here

                    }else{
                        session::set('error_general', 'Something went wrong.');
                    }
                }else{
                    session::set('error_email', 'This email is already used.');
                }

            }else{
                session::set('error_repassword', 'The two passwords are not the same.');
            }

        }else{
            foreach ($return as $field => $error) {
                session::set($field, $error);
            }
        }

    }

Like includes/log-user-in.php, this file is in charge of intercepting user's action when he wants to sign up.

It includes the database class and the model class. it uses the validateForm() too to validate the form. Checks if the email the user provided is not already used via emailAlreadyUsed(). If everything is well, it will save the user into our members table with addUser().

  • dashboard.php

`

<?php 
        require __DIR__. '../includes/header.php'; 
        !isset($_SESSION['user_session']) ? header("location:index.php"):null;
    ?>

    <div class="row">
        <div class="col s12 offset-s1 m12 l12 z-depth-1 white">
            <?php $user = explode("@", $_SESSION['user_session']) ?>
            Welcome, <b><?= isset($user[0]) ? $user[0] : '' ?></b>

            <div class="divider"></div>

            <h2>Congratulations</h2>
            <p>This is a dashboard than can only be access by logged memebers.</p>
            <p>If you are here, it means you created an account and logged in successfully.</p>

            <div class="divider"></div>
            <p>If you want to log out, use the button bellow:</p>
            <a href="logout.php" class="waves-effect waves-light btn">Log out</a>
        </div>
    </div>

    <?php require __DIR__. '../includes/footer.php'; ?>

This file is only accessible by users who are logged in. It actually doesn't do any great thing. It takes the user email from the $_SESSION['user_session'] variable then cuts the @domaine.tld part and displays the left side of the email. So if your email is test@gmail.com, when you log in, the dashboard will welcome you like: Welcome, test.

Aside from that the dashboard also provides a logout button to help a user close his session.

Basically, the dashboard should implement all actions that are proper to logged in users. Things you don't want guess users to do, you implement them there.

But the main thing about the dashboard/private pages and many beginners are finding hard to do is the possibility of revoking access to nun-logged users at some parts of their website.

Here is the key:

!isset($_SESSION['user_session']) ? header("location:index.php"):null;

Meaning: if any user comes to a page in which there is this line and that user did not log first, he should be redirected automatically to index.php. So if there a page you don't want guest users to access just add this line.

Note: The name of the SESSION constant (user_session) here is my personal choice. It it created in includes/log-user-in.php with session::set('user_session', $email);

  • logout.php

`

<?php 
        require __DIR__. '../includes/header.php'; 

        if (isset($_SESSION['user_session'])) {
            session::destroy('user_session');
            header('location:index.php');
        }

        require __DIR__. '../includes/footer.php'; 
    ?>

This file is the last file. it helps a user to log out and stop his session by clicking the logout button in the dashboard.

I purposely included the includes/header.php and includes/footer.php files to allow the logout page to benefit from our website's layout. This could be very useful in case the user remains on the logout page and sees a message like you have logged out successfully.

In my case even though I have included the layout, I still redirect my users to my index.php page.

Room for improvement

This can't be called perfect. There is still a lot if we want to implement a serious membership system. But these basics can help you understand the concept and how to make it with some home-made code.

Here are things we could add or make perfect:

  1. keep the user session alive for a while if he checks on remember me
  2. Send a confirmation email to the user when he creates his account
  3. Allow the user to log in if only he has confirmed his email
  4. Allow a user to recover his password in case he forgets it.
  5. Display user first name and last name on the dashboard instead of displaying a part of his email.
  6. Work out our design
  7. Work out the way we handle our error messages
  8. Add form validation with some ajax
  9. Give more options to our users in their dashboard
  10. Reinforce the password encryption
  11. Add more criteria to our forms validation
  12. Use some good MVC design pattern to make our files more maintainable and well organized
  13. Consider the size/strength of the password

Conclusion

We have arrived at the end of this part. I believe many will receive its light and start understanding how these things are done.

This is a tutorial, not a framework that you can take and use for your commercial projects without looking at improving it in terms of security and organization of the code. This shows you how are things work. If you want to use it for a commercial purpose, use a php framework. You can also download my source code to compare with what you've got.

In the next part of this tutorial, I will try to show you how to make theremember me and Forgot password functionalities.

Also, I am certain I neglected somethings that matter a lot for you in such system. I am expecting you to tell me what that's and I will do my best with the help of the community to solve it for you.

Hope that was helpful.

Last updated 2024-01-11 UTC