A complete tutorial on Admin Routing for CakePHP

in CakePHP/PHP/Tutorials & Samples

Although CakePHP’s cookbook does a pretty good job at explaining admin routing, I’ve still received requests for a tutorial on admin routing. So, for my very first tutorial for 2015, I will be showing you a complete tutorial for admin routing in CakePHP. The tutorial will be a combination of the user authentication application that we built in a previous tutorial and the CakePHP blog tutorial in the cookbook. In this tutorial, we will build a blog application that contains a frontend section that all users can see and a backend admin section where administrators and authors go to create new blog posts. You can think of our application as a mini-WordPress. (I personally love WordPress and their application is one of the reasons why I go interested in web development) Below are screenshots of what we will be building:

front-section

admin-section

You can view a demo here and download the source code here. The version of CakePHP that I am using for this application is 2.6. The code should still work on other versions of Cake though.

Authorization vs. Authentication

In order to implement proper admin routing, you need to setup both authentication and authorization. Before we begin this tutorial, it’s important to know the difference between authorization and authentication. Here are the definitions:

Authentication: Authentication tells us who is allowed to access the application and who is not. Think of it as the login screen that blocks unauthorized users from logging in to the system. Authentication must happen before authorization can take place.

Authorization: Authorization tells users that have been authenticated what resources that they have access to. For example, a logged-in user who is not an administrator may not have access to certain resources that are only reserved for administrators.

The Database Setup

As usual, we will begin the tutorial by defining our databases. We need two tables for this tutorial: Users and Posts. Posts have a title and a body and they are created by users (that is why there is field user_id to identify who created the post.) Users are required to authenticate into the system and they have a username, password, email and bio. Below are both tables

CREATE TABLE posts (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(256),
    body TEXT,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL,
    status TINYINT DEFAULT 1
);

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(128),
    password VARCHAR(256),
    email VARCHAR(128),
    role ENUM('admin', 'author') NOT NULL,
    bio TEXT,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL,
    status TINYINT DEFAULT 1
);

Configuration updates

Two configuration files need to be updated in order for us to have proper admin routing

Routes.php

In is routes.php, we need to setup some basic routes to handle the authentication routes. Here is what was added in routes.php

	Router::connect('/', array('controller' => 'posts', 'action' => 'index', 'home'));
	Router::connect('/admin', array('controller' => 'users', 'action' => 'dashboard', 'admin' => true));
	Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
	Router::connect('/logout', array('controller' => 'users', 'action' => 'logout', 'admin' => true));
	Router::connect('/setup', array('controller' => 'users', 'action' => 'setup'));

We now have defined routes for login, logout, dashboard and the home page. There is an additional page called setup that we created a route for. We will explain it later.

Core.php

We need to turn on admin routing. This is done in core.php with the following line of code:

	Configure::write('Routing.prefixes', array('admin'));

What this piece of code does is tell CakePHP that we want prefix routing turned on and that the prefix we will be using is ‘admin’. So now, if we have a controller with the prefix admin_, CakePHP will automagically re-route to add the admin prefix. For example, if we created a controller function called admin_delete() in users, CakePHP would automatically know that the route for this controller is /users/admin/delete/

In the configuration file, you can add as many prefixes as you like. So, let’s say that we wanted another prefix for managers, then we could simply add ‘manager’ to our array of prefixes. Then we could have a controller called manager_delete().

Handling Authentication and Authorization

Although we have modified the configuration files to enable admin routing, it is still our responsibility to actually handle the authentication and authorization. In order words, creating a controller function called admin_delete() will tell CakePHP to create the path /users/admin/delete/ but CakePHP will not know who should have access to this function and who should not. To take care of this, we will make some modifications to the base controller class: AppController. AppController is where we will:

  • Setup the Auth component
  • Determine who should have access to what pages
  • Determine how to render front pages vs admin pages
  • Setup the authorization rules

Here are the important contents of our AppController.php file:


	public $components = array(

    	DebugKit.Toolbar',
		'Session',
		'Auth' => array(
			'loginAction' => array('controller'=>'users','action'=>'login', 'admin'=>false),
			'loginRedirect' => array( 
				'controller' => 'users',
				'action' => 'dashboard',
				'admin' => true
            ),
            'logoutRedirect' => array( 
				'controller' => 'users',
				'action' => 'login',
				'admin'=> false  // add this so that admin actions get ignored
            ),
			'authError' => 'Access Denied',
			'authenticate' => array(
				'Form' => array(
					'passwordHasher' => 'Blowfish'
				)
			),
			'authorize' => array('Controller')
        )
    );

The first part of AppController.php is where I setup the components that I would like to use. I am currently using the DebugKit’s toolbar and the Auth component. With the Auth component setup is as follows:

  • loginAction: This is where I define my login action to be the login function in the Users controller
  • loginRedirect: On a successful login, the user should be redirected to the dashboard function in the Users controller
  • logoutRedirect: On a logout, the user should be redirected to the login function in the Users controller
  • authError: For any error that occurs in the Auth component, we should display ‘Access Denied’
  • authenticate: Our authentication should be via a form and the password hasher I will be using is the Blowfish
  • authorize: For authorization requests, it will be handled by the various controller.
	public function beforeFilter() {
		// Auth will block all entries with admin prefix unless the user is authenticated
        if(isset($this->request->prefix) && ($this->request->prefix == 'admin')){
         	if($this->Auth->loggedIn()){
            	$this->Auth->allow();
                $this->layout = 'admin';
            }else{
				$this->Auth->deny();
                $this->layout = 'front';
            }
        }else{
			$this->Auth->allow();
			$this->layout = 'front';
        }
	}

The second part of AppController is defining a way to handle admin pages vs regular front pages. I want only logged-in members to have access to the pages that are prefixed with ‘admin’ and all users to have access to all the other sections of the site. However, I do not want to set this up for every function that I write. So instead, I will define this behaviour inside the beforeFilter() function which is called everytime that a controller function is loaded. And because of CakePHP’s Object-Oriented nature, all Controllers inherit this function from the base class so writing this function once makes it available to all future controllers that I create. Basically, this function simply checks to see if the admin prefix is present. If the prefix is present and we are a logged-in member, then Auth will grant access to the page, otherwise Auth will throw an error and inform the user that they are not authorized. Additionally, I have setup a front layout and an admin layout. Depending on whether the admin prefix is present, the beforeFilter() function will chose which layout to use. Once again, this avoids having to set the layout in every single function that I write. However, I can still overwrite this layout if I ever need to do so in any controller function.

	public function isAuthorized($user = null) {
		// Everyone is authorized to see that front pages. However, some admin pages require you to be an admin to have access
		if(isset($this->request->prefix) && ($this->request->prefix == 'admin')){
			if($this->Auth->loggedIn()){
				if($this->Auth->user('role') == 'admin'){
					return true;
				}else{
					return false;
				}
			}else{
				return false;
			}
		}
		return true;
    }

The final thing that I do is define the base isAuthorized() function. For those who are unaware, isAuthorized() is a base function that CakePHP offers that allows you to setup any condition that you want for the authorization. Just like beforeFilter(), this function is called everytime that a controller function is loaded. Above is my base definition which basically says that everybody is allowed to access all pages except the pages that are prefixed by the ‘admin’. In the case of pages that are prefixed by ‘admin’, only users with the role ‘admin’ are allowed to enter. (For this tutorial, we have 2 user roles: admin and author since this is a blog application).

Adding the loggedIn() check is probably overkill since our beforeFilter() function already checks to see that the user is loggedIn in order to gain access to the ‘admin’ section. And the only people who are granted authorization to the full admin section are users with the ‘admin’ role. That is because we will be overwriting the isAuthorized function inside our various controllers and inheriting from this base function, which you can consider our starting point.

Users Updates

Now that routes have been defined, prefix routing is enabled and our base class is setup, we are ready to start modifying our application to handle authentication. To get authentication up and running, we need to update the Users model as well as the Users controller.

User.php

User.php is an exact copy of the version that we had in the previous tutorial, the only difference is that we are now using the Blowfish password hasher, which is more secure than the traditional password hasher. Below is the full content of the User.php file. If there is anything that you don’t understand, please see the previous tutorial since it is probably explained in that tutorial.

// app/Model/User.php
App::uses('AppModel', 'Model');
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');

class User extends AppModel {

	public $validate = array(
        'username' => array(
			'nonEmpty' => array(
				'rule' => array('notEmpty'),
				'message' => 'A username is required',
				'allowEmpty' => false
            ),
			'between' => array(
				'rule' => array('between', 5, 15),
				'required' => true,
				'message' => 'Usernames must be between 5 to 15 characters'
			),
			'unique' => array(
				'rule'    => array('isUniqueUsername'),
				'message' => 'This username is already in use'
			),
			'alphaNumericDashUnderscore' => array(
				'rule'    => array('alphaNumericDashUnderscore'),
				'message' => 'Username can only be letters, numbers and underscores'
			),
        ),

        'password' => array(
            'required' => array(
				'rule' => array('notEmpty'),
				'message' => 'A password is required'
            ),
			'min_length' => array(
				'rule' => array('minLength', '6'), 
				'message' => 'Password must have a mimimum of 6 characters'
            )
        ),

		'password_confirm' => array(
            'required' => array(
                'rule' => array('notEmpty'),
                'message' => 'Please confirm your password'
            ),
			'equaltofield' => array(
				'rule' => array('equaltofield','password'),
				'message' => 'Both passwords must match.'
			)
        ),

		'email' => array(
			'required' => array(
				'rule' => array('email', true),   
				'message' => 'Please provide a valid email address.'   
			),
			'unique' => array(
				'rule'    => array('isUniqueEmail'),
				'message' => 'This email is already in use',
				),
			'between' => array(
				'rule' => array('between', 6, 60),
				'message' => 'Usernames must be between 6 to 60 characters'
			)
		),

        'role' => array(
            'valid' => array(
				'rule' => array('inList', array('admin', 'author')),
                'message' => 'Please enter a valid role',
                'allowEmpty' => false
            )
        ),

		'password_update' => array(
			'min_length' => array(
			'rule' => array('minLength', '6'),  
			'message' => 'Password must have a mimimum of 6 characters',
			'allowEmpty' => true,
			'required' => false
			)
        ),

        'password_confirm_update' => array(
			'equaltofield' => array(
				'rule' => array('equaltofield','password_update'),
				'message' => 'Both passwords must match.',
				'required' => false,
        	)
    	);


		/**
        * Before isUniqueUsername
		* @param array $options
		* @return boolean
		*/
		function isUniqueUsername($check) {
			$username = $this->find(
				'first',
				array(
						'fields' => array(
						'User.id',
						'User.username'
					),
				'conditions' => array(
						'User.username' => $check['username']
					)
				)
			);

            if(!empty($username)){
				if($this->data[$this->alias]['username'] == $username['User']['username']){
					return true;
				}else{
					return false;
				}
			}else{
				return true;
			}
		}

        /**
		* Before isUniqueEmail
		* @param array $options
		* @return boolean
		*/
		function isUniqueEmail($check) {
			$email = $this->find(
				'first',
				array(
					'fields' => array(
						'User.id',
						'User.email'
					),
					'conditions' => array(
						'User.email' => $check['email']
					)
				)
			);

            if(!empty($email)){
				if($this->data[$this->alias]['email'] == $email['User']['email']){
					return true;
				}else{
					return false;
				}
			}else{
				return true;
			}
    	}



		public function alphaNumericDashUnderscore($check) {
	        // $data array is passed using the form field name as the key
	        // have to extract the value to make the function generic
	        $value = array_values($check);
	        $value = $value[0];
	        return preg_match('/^[a-zA-Z0-9_ \-]*$/', $value);

    	}



        public function equaltofield($check,$otherfield){
	        //get name of field
	        $fname = '';

	        foreach ($check as $key => $value){
	            $fname = $key;
	            break;
	        }
        	return $this->data[$this->name][$otherfield] === $this->data[$this->name][$fname];
    	}

        /**
        * Before Save
        * @param array $options
        * @return boolean
        */
		public function beforeSave($options = array()) {
			// hash the user's password befor we save it
			if (isset($this->data[$this->alias]['password'])) {
				$passwordHasher = new BlowfishPasswordHasher();
				$this->data[$this->alias]['password'] = $passwordHasher->hash(
					$this->data[$this->alias]['password']
				);

            }

			// if we get an updated password, hash it
			if (isset($this->data[$this->alias]['password_update'])) {
				$passwordHasher = new BlowfishPasswordHasher();
                $this->data[$this->alias]['password'] = $passwordHasher->hash(
                	$this->data[$this->alias]['password_update']
			    );
            }
             
			// fallback to our parent
            return parent::beforeSave($options);
		}
}

UsersController.php

Although the User model does not change much, UsersController changes significantly from the previous tutorial. We now have 2 types of controllers: Those requiring you to be logged-in to the system to gain access and those that anybody can access. The functions admin_logout(), admin_dashboard(), admin_profile(), admin_profile_edit(), admin_index(), admin_add(), admin_edit() and admin_delete() all require the user to be authenticated to access them and they all use the admin routing that we setup earlier to build proper URLs for us.

Below is the full source code for UsersController.php followed by explanations for the important functions.


// app/Controller/UsersController.php

App::uses('AppController', 'Controller');

class UsersController extends AppController {

	public $paginate = array(
		'limit' => 25,
        'conditions' => array('status' => '1'),
		'order' => array('User.username' => 'asc' )
    );


    public function beforeFilter() {
        parent::beforeFilter();
        // do any additional beforeFilter stuff after calling the parent function

    }

	public function login() {
		//if already logged-in, redirect
		if($this->Session->check('Auth.User')){
			return $this->redirect(array('action' => 'index'));                              
		}

		// if we get the post information, try to authenticate
		if ($this->request->is('post')) {
			if ($this->Auth->login()) {
				$status = $this->Auth->user('status');
				if($status != 0){
					$this->Session->setFlash(__('Welcome, '. $this->Auth->user('username')));
					return $this->redirect($this->Auth->redirectUrl());
				}else{
					// this is a deleted user
					$this->Auth->logout();
					$this->Session->setFlash(__('Invalid username or password - This user appears to be deleted...'));
				}
			} else {
					$this->Session->setFlash(__('Invalid username or password'));
			}
		}
	}

	public function setup() {
		// check to see if we already have a user created...
		$firstUser = $this->User->find('first');
		if(!empty($firstUser)){
			// if we alredy have a user, we can no longer show this page but we should let the user know
			$this->Session->setFlash(__('The Initial administrator has already been setup. Please login instead.'));
			if($this->Auth->loggedIn()){
				return $this->redirect(array('action' => 'admin_dashboard'));
			}else{
				return $this->redirect(array('action' => 'login'));
			}
		}

		if ($this->request->is('post')) {

			$this->User->create();
			if ($this->User->save($this->request->data)) {
				$this->Session->setFlash(__('The initial adminstrator has been created succesfully! You can login now.'));
				return $this->redirect(array('action' => 'login'));
            } else {
				$this->Session->setFlash(__('The initial user could not be created. Please, try again.'));
			}
		}
    }

    public function admin_logout() {
		return $this->redirect($this->Auth->logout());
	}

	public function admin_dashboard() {
		// nothing done here, everything happens in the view
	}

	public function admin_profile(){
		// nothing done here, everything happens in the view
		$user = $this->User->findById($this->Auth->user('id'));
		$this->set(compact('user'));
	}

	public function admin_profile_edit(){
		$user = $this->User->findById($this->Auth->user('id'));
		if ($this->request->is('post') || $this->request->is('put')) {
			$this->User->id = $this->Auth->user('id');
			if ($this->User->save($this->request->data)) {
				$this->Session->setFlash(__('Your profile data has been updated'));
				return $this->redirect(array('action' => 'admin_profile_edit'));
			}else{
				$this->Session->setFlash(__('Unable to update your profile.'));
			}
		}

		if (!$this->request->data) {
			$this->request->data = $user;
		}

    }

    public function admin_index() {
        $this->paginate = array(
			'limit' => 10,
			'order' => array('User.username' => 'asc' ),
			'conditions' => array('User.status' => 1),
         );

		$users = $this->paginate('User');
		$this->set(compact('users'));
    }

    public function admin_add() {

    	if($this->isAuthorized()){
        	if ($this->request->is('post')) {
            	$this->User->create();
                if ($this->User->save($this->request->data)) {
                	$this->Session->setFlash(__('The user has been created'));
                    return $this->redirect(array('action' => 'index'));
				} else {
					$this->Session->setFlash(__('The user could not be created. Please, try again.'));
                }
			}
		}else{
			$this->Session->setFlash(__('You do not have permission to do this'));
			return $this->redirect(array('action' => 'admin_dashboard'));
		}
    }

    public function admin_edit($id = null) {
		if($this->isAuthorized()){
			if (!$id) {
				$this->Session->setFlash('Please provide a user id');
				return $this->redirect(array('action'=>'index'));
			}

            $user = $this->User->findById($id);

            if (!$user) {
				$this->Session->setFlash('Invalid User ID Provided');
				return $this->redirect(array('action'=>'index'));
			}

            if ($this->request->is('post') || $this->request->is('put')) {
				$this->User->id = $id;
				if ($this->User->save($this->request->data)) {
					$this->Session->setFlash(__('The user has been updated'));
					return $this->redirect(array('action' => 'edit', $id));
				}else{
					$this->Session->setFlash(__('Unable to update your user.'));
				}
			}

            if (!$this->request->data) {
				$this->request->data = $user;
			}
		}else{
			$this->Session->setFlash(__('You do not have permission to do this'));
			return $this->redirect(array('action' => 'admin_dashboard'));
		}
    }

    public function admin_delete($id = null) {
		if($this->isAuthorized()){
			if (!$id) {
				$this->Session->setFlash('Please provide a user id');
				return $this->redirect(array('action'=>'admin_dashboard'));
			}

			$this->User->id = $id;
			if (!$this->User->exists()) {
				$this->Session->setFlash('Invalid user id provided');
				return $this->redirect(array('action'=>'admin_dashboard'));
			}
			
			if ($this->User->saveField('status', 0)) {
				$this->Session->setFlash(__('User deleted'));
				return $this->redirect(array('action' => 'admin_dashboard'));
			}

			$this->Session->setFlash(__('User was not deleted'));
			return $this->redirect(array('action' => 'admin_dashboard'));

		}else{
			$this->Session->setFlash(__('You do not have permission to do this'));
			return $this->redirect(array('action' => 'admin_dashboard'));
		}

    }

}

Now, lets talk about the important functions and parts of UsersController.php

Setup()

Earlier in this tutorial, I mentioned that we would explain what the setup function does. Well, the setup function allows you to create your first administrator on the system so that you can login to the system. Setup() will only work if no user exists in the application and it will allow you to create the first user. This is a technique very similar to what is done with WordPress. When you first install WordPress, the installation process will ask you to create the default administrator. We achieve this with the setup() function. And just like WordPress, once an administrator has been created, you will no longer be allowed to access this function ever again.

Admin_profile() and Admin_profile_edit()

Both of these functions allow a logged-in user to view and edit their profile. Any user, regardless of their role can view and update their profile. This is quite different from the admin_edit() function which requires you to have the admin role in order to access it.

Admin_add() and Admin_edit()

Both of these function require you to be have the role admin in order to have access to them. That is why the first thing that is done is to call the isAuthorized() function.

Notice how I do not overwrite the isAuhorized() and beforeFilter() functions. However, I do provide an example of how to override any of these two function by providing you a skeleton of how to override the beforeFilter() function. In my example, I call the parent and then leave space for any additional checks that I want to do

	public function beforeFilter() {
		 parent::beforeFilter();

		// do any additional beforeFilter stuff after calling the parent function
    }

Posts Updates

There are really not that many changes that I have made to the Posts model and controller other than add the additional field of user_id so that we know who a particular post belongs to.

Post.php

I have added the association to users with the belongsTo keyword and copied the function isOwnedBy() directly from the cookbook since this function is useful in determining who owns a particular post. Other than that, the model is pretty basic.

class Post extends AppModel {
	public $belongsTo = 'User';

    // function to determine if the provided user is the owner of the provided post
	// copied as-is from http://book.cakephp.org/2.0/en/tutorials-and-examples/blog-auth-example/auth.html

	public function isOwnedBy($post, $user) {
		return $this->field('id', array('id' => $post, 'user_id' => $user)) !== false;
	}
}

PostController.php

The posts controller follows the same logic as the users controller in that there are certain functions that are only available to admins and that all administration pages are preceded by ‘admin_’. However, the definition of authorized changes in the case of posts. For posts, any user with the role ‘admin’ can do anything. But for all users with the role ‘author’, they can only modify and delete posts that belong to them. That is why I copied the function isOwnedBy() for this tutorial. Below is a look at the overridden isAuthorized() function used for posts


	public function isAuthorized($user = NULL) {
		// Unless they are an admin, only the owner of a post can edit or delete it
		if (in_array($this->action, array('admin_edit', 'admin_delete')) && ($this->Auth->user('role') != 'admin')) {
			$postId = (int) $this->request->params['pass'][0];
			if($this->Post->isOwnedBy($postId,$this->Auth->user('id'))){
				return true;
			}
		}

		return parent::isAuthorized($user);
    }

This inherited function is pretty easy to understand. And it follows the very important principle of inheritance: if ever the condition that I am looking for is not met, I fall back to my base version that is defined in AppController.php

Conclusion

That’s all there is to having proper admin routing in php while using the principles of Object Oriented programming to keep your code simple and re-usable. You can download the full tutorial here and you can play with the demo here.

Tags:

Mifty Yusuf is a Montreal-based software developer who enjoys playing with new web technologies as well as comic books and illustrations. He beleives that, no matter what the question is, the answer is always Batman!

12 Comments

  1. How can i only show posts related to author only. On the author admin sceen it shows up all posts.
    i want to show only posts related to that author

    • Hi lenne. THis tutorial was created for Cake 2.x and will never work on 3.x without modifications. unfortunately, I am moving from CakePHP to Laravel. So, most likely, I will not be proting this tutorial to CakePHP 3.0

  2. Why are you moving away from Cake? Any specific reason? And something was off with your “Leave a Reply” it was off screen to me…

    • Hi Bojan. Well, I’m looking at the various PHP development frameworks and it is becoming more and more clear that Laravel is the new king. So, instead of focusing on CakePHP 3.0, I’ll be moving on to Laravel.

    • This will not work for CakePHP 3 since the structure has changed. I have moved on to Laravel so I am not sure how this would get done in CakePHP 3. Sorry 🙁

  3. Excellent post. Keep posting such kind of information on your blog.

    Im really impressed by your blog.
    Hey there, You’ve performed a fantastic job.
    I will definitely digg it and in my view recommend to my friends.
    I’m confident they’ll be benefited from this web site.

Leave a Reply

Your email address will not be published.

*

Latest from CakePHP

Go to Top