Creating an Admin Section with CakePHP Updated

CakePHP Login Screen

My previous post on this topic is the most viewed article on the website so I thought I would re-visit it and do it all again using the latest version of Cake which at the time of writing is 1.2.3.8166

I'm going to be using a similar method as last time but a lot more refined, mainly because I'm improving as a programmer and that I'm getting more familiar with CakePHP and the best practises with using it.

Edit: I've released an updated post and sample template application to deal with CakePHP 1.3.3 go here to see it

User Authentication

I'm aware that Cake comes with a core Authentication component but I've never used it for some reason, I think it's because I want full control over what happens so I'm going to show you a fairly standard way of authenticating users and allowing them access to a password protected area of your application.

Enable Admin Routing

First thing you need to do is enable admin routing in Cake. Open up core.php and uncomment line 67:

// file: /app/config/core.php
Configure::write('Routing.admin', 'admin');

This will make sure that a url such as /admin/posts will get routed to the correct location. As a side note if you change the value of the "Routing.admin" variable then you will also need to change your Controller actions with the same name.

Users SQL

Below is some SQL code to create a very standard table for holding your User information. All passwords are going to be md5 hashed for security purposes.

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `last_login` datetime NOT NULL,
  `status` tinyint(1) DEFAULT '1',
  PRIMARY KEY (`id`)
);

Users Model

This is where the form validation takes place. The validate array contains rules for both the "username" and "password" fields and checks that the user has inputted something for both fields. The "allowEmpty" key ensures that the form will not validate unless data has been entered into the form.

I've also created a method check_user_data() that will take the form data as an argument and will try to find a user in the database with the same username and password. The method first finds a User with a matching username and then compares passwords. If a User was found then the data is returned, otherwise the method returns FALSE. As a quick side note the passwords in the database are hashed using md5 and to make things a little more secure the password is combined with a "salt" variable which is defined in the core.php file.

<?php
// file: /app/models/user.php
class User extends AppModel {
	var $name = 'User';
	var $validate = array(
		'username'=>array(
			'rule'=>VALID_NOT_EMPTY,
			'required'=>true,
			'allowEmpty'=>false,
			'message'=>'Please enter your Username'
		),
		'password'=>array(
			'rule'=>VALID_NOT_EMPTY,
			'required'=>true,
			'allowEmpty'=>false,
			'message'=>'Please enter your Password'
		)
	);


	/**
	 * Checks User data is valid before allowing access to system
	 * @param array $data
	 * @return boolean|array
	 */
	function check_user_data($data) {
		// init
		$return = FALSE;

		// find user with passed username
		$conditions = array(
			'User.username'=>$data['User']['username'],
			'User.status'=>'1'
		);
		$user = $this->find('first',array('conditions'=>$conditions));

		// not found
		if(!empty($user)) {
			$salt = Configure::read('Security.salt');
			// check password
			if($user['User']['password'] == md5($data['User']['password'].$salt)) {
				$return = $user;
			}
		}

	return $return;
	}
}
?>

Users Controller

This is where most of the login logic is taking place. The login() action first checks so see if a User is already logged in, if so the user will be redirected to the dashboard. Then it checks if the form has been submitted, validates the data, checks that the User exists in the database and if it does the action will update the last login date and save the User's data to the Session.

I'm using the Model validates() method to check that the User data is valid, this is called automatically when saving data but if your going to use this you have to set the form data to the Model by using the set() method.

The logout() action simply deletes the Session User data and redirects back to the login page. Nothing fancy but it gets the job done. I've also included the beforeFilter() method which calls the parent method incase you want to include any other code before any of the Controller actions.

I've also commenting out the first few lines of the login action which will print out an md5 password which you can use to insert a new user into the database.

<?php
// /app/controllers/users_controller.php

class UsersController extends AppController {
	var $name = 'Users';
	var $helpers = array('Html', 'Form');

	/**
	 * Before any Controller Action
	 */
	function beforeFilter() {
		parent::beforeFilter();
	}


	/**
	 * Logs in a User
	 */
	function login() {
		//$salt = Configure::read('Security.salt');
		//echo md5('password'.$salt);

		// redirect user if already logged in
		if( $this->Session->check('User') ) {
			$this->redirect(array('controller'=>'dashboard','action'=>'index','admin'=>true));
		}

		if(!empty($this->data)) {
			// set the form data to enable validation
			$this->User->set( $this->data );
			// see if the data validates
			if($this->User->validates()) {
				// check user is valid
				$result = $this->User->check_user_data($this->data);

				if( $result !== FALSE ) {
					// update login time
					$this->User->id = $result['User']['id'];
					$this->User->saveField('last_login',date("Y-m-d H:i:s"));
					// save to session
					$this->Session->write('User',$result);
					$this->Session->setFlash('You have successfully logged in','flash_good');
					$this->redirect(array('controller'=>'dashboard','action'=>'index','admin'=>true));
				} else {
					$this->Session->setFlash('Either your Username of Password is incorrect','flash_bad');
				}
			}
		}
	}


	/**
	 * Logs out a User
	 */
	function logout() {
		if($this->Session->check('User')) {
			$this->Session->delete('User');
			$this->Session->setFlash('You have successfully logged out','flash_good');
		}
		$this->redirect(array('action'=>'login'));
	}
}
?>

Login View

This is quite a simple page that displays a form with fields for the "username" and "password" to enable users to login to your application. When the form is created please note that the "login" action is defined so that the data gets sent to the correct Controller action.

// file: /app/views/users/login.ctp

<div class="login">
	<?php echo $form->create('User',array('action'=>'login'));?>
	<fieldset>
 		<legend>Enter Your Username and Password</legend>
		<?php
		echo $form->input('username');
		echo $form->input('password');
		?>
		<div class="input buttons">
			<button type="submit" name="data[User][login]" value="login">Login</button>
		</div>
	</fieldset>
<?php echo $form->end();?>
</div>

Checking a User is Logged In

To check that a user is logged in I'm going to create an app_controller.php file that will be accessible by every Controller in your application. Any methods that you create here can be called by $this->ControllerName->method()

The beforeFilter() method is a special CakePHP method that will be called automatically before any Controller action. First I'm going to check that an admin url has been requested by checking the "params" array. Then I'm going to check the Session for a variable named "User", this was saved when the user logged into the application. If it doesn't exist then we can assume that the user is not logged in.

If a user is logged into the application then I'm going to save the User data into a class variable. This will enable me to access the User data from any Controller by looking in the $this->Controller->_User variable. Finally I'm going to change the layout the application uses incase the admin area has a different style from the main layout.

<?php
// file: /app/app_controller.php

class AppController extends Controller {
	// class variables
	var $_User = array();

	/**
	 * Before any Controller action
	 */
	function beforeFilter() {
		// if admin url requested
		if(isset($this->params['admin']) && $this->params['admin']) {
			// check user is logged in
			if( !$this->Session->check('User') ) {
				$this->Session->setFlash('You must be logged in for that action.','flash_bad');
				$this->redirect('/login');
			}

			// save user data
			$this->_User = $this->Session->read('User');
			$this->set('user',$this->_User);

			// change layout
			$this->layout = 'admin';
		}
	}
}
?>

Note that if you create another beforeFilter() method in your Controller you must call the parent method so that it also gets called like this:

parent::beforeFilter();

Custom Routes

I'm going to define a few custom routes for the "login" and "logout" actions so I don't have to going through the users Controller. Not very significant but it makes for a clean application.

// file: /app/config/routes.php

Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
Router::connect('/admin/logout', array('controller' => 'users', 'action' => 'logout'));

Wrapping Up

Hopefully most of the above makes sense and if not just comment below and I'll try and help you out. This method is quite streamlined compared to my previous article and the code is much cleaner and easier to understand and maintain.

Edit: I've released an updated post and sample template application to deal with CakePHP 1.3.3 go here to see it

Posted on 29th May 2009
5 years, 6 months, 3 weeks, 2 days ago

comments powered by Disqus