Creating an Admin Section with CakePHP Updated
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
Comments
kicaj (29/05/2009 - 16:34)
Hi, thanks a lot! I waiting for new version of this...
mupet (31/05/2009 - 09:19)
nice articles, i like this very much, this is the best blog about cakephp i ever seen
Peter Ojiambo (01/06/2009 - 12:48)
Thanks for the great tutorials!
your site is a great resource for learning cakephp!
can you please further clarify why you "set the form data to enable validation", line30 in the users controller. Cant you just assign the values in the form to a new user object being created?
Cant seem to get my head around that, please help clarify.
Please keep the tutes coming!
Thanks!
James (01/06/2009 - 14:02)
@Peter: Hey thanks for commenting, line 30 is just setting all the form data to the User Object in one go. You can read more about it in the online manual @ http://book.cakephp.org/view/410/Validating-Data-from-the-Controller
Hope that helps.
George (04/06/2009 - 03:10)
Hey James, great site and great Cake tutorials. A couple of things I have noticed though (1) how come you're using the old Cake validation methods of VALID_NOT_EMPTY etc when these are much more refined and flexible now? Also (2) how come you hash user passwords manually when the Auth component does this automatically to the salt value in the install?
Just a couple of things that baffled me but I thought you probably must have a reason for them as you seem to be pretty talented with using the framework etc.
All the best and keep up the good work mate.
James (04/06/2009 - 11:25)
@George: Thanks for getting touch and commenting.
1) I've had problems with the validation rules and specifically 'notEmpty' so it's just easier using the old VALID_NOT_EMPTY rule, obviously people are free to use the updated rules.
2) I've never tried using the AuthComponent but I can imagine its quite easy to use, I'll have to try it out sometime.
I've just visited your site and I like it, I'm looking forward to the Cake tutorials you have in the pipeline.
Sri (11/06/2009 - 08:34)
Thanks for the tutorial. I'm a little stuck in one area. I have created a login page which renders, but when I login, it goes to a "blank" page with the url of /login still.
Also if I navigate to an admin section it takes me to /login, but it has a blank page.
Am I missing something?
HaYsSeN (20/07/2009 - 13:51)
really i like the tutorial, thx for it ;), but i get the same problem of the blank page!!! do you have any idea about the reason?
HaYsSeN (20/07/2009 - 15:43)
ok i find the cause,
The signature of Session->setFlash() has changed. So the new signature is:
function setFlash($flashMessage, $layout = 'default', $params = array(), $key = 'flash')
Rafael (26/08/2009 - 05:33)
Does the method described in post "Creating an Admin Section with CakePHP Updated" works in this new version of cakephp?
James (02/09/2009 - 23:13)
@Rafael: Sorry for the slow response, I haven't tested the multiple admin code with the latest version of CakePHP but I shouldn't see why it wouldn't work. It may need modifying slightly but the general logic will be the same.
Bruno (14/10/2009 - 09:27)
Hello James, thanks for this tutorial, I have a question though... so far everything works perfectly except, the landing location for http://cakephp/admin/ is returning errors i.e:
Missing Controller
Error: Controller could not be found.
Error: Create the class Controller below in file: app\controllers\controller.php
<?php
class Controller extends AppController {
var $name = '';
}
?>
Any ideas how I can make a landing location for that to be in an admin_controller.php ?
James (14/10/2009 - 10:01)
@Bruno: Hi and thanks for commenting. It sounds like you're doing something wrong. The "admin" is supposed to go in your URL before the Controller name, for instance:
http://cakephp/admin/posts
And then in your "posts_controller.php" you will have a function called "admin_index()" that will be called when the URL is requested. Unless I've misunderstood you completely and that's not why you're having problems.
Arno (02/11/2009 - 00:12)
@bruno, @james
In your routes.php file, you can set up a default route for the admin section, similar to the default routes set up fir the non-admin
in /app/config/routes.php
//default routes
Router::connect('/', array('controller' => 'posts', 'action' => 'index'));
Router::connect('/posts/', array('controller' => 'posts'));
//admin routes
Router::connect('/admin/', array('controller' => 'posts', 'action' => 'index', 'admin' => true));
Router::connect('/admin/posts/', array('controller' => 'posts', 'admin' => true));
KHAJA (09/11/2009 - 08:43)
Thanks for the tutorial. I'm a little stuck in one area. I have created a login page, but when I login, it goes to a "blank" page with the url of "http://localhost/cake/login" still.
Am I missing something?
"
HaYsSeN (20/07/2009 - 15:43)
ok i find the cause,
The signature of Session->setFlash() has changed. So the new signature is:
function setFlash($flashMessage, $layout = 'default', $params = array(), $key = 'flash')
"
Where can I plae this setFlash? Please help me.
Thanks in Advance.
James (09/11/2009 - 22:38)
@KHAJA: I made that mistake too, you can just use the setFlash() as normal but you'll need to change the way it works. These days I call this and pass a class parameter which can then style the message with CSS:
$this->setFlash('message','default',array('class'=>'flash_good'));
Raja (24/11/2009 - 22:50)
Thank You. its working great.
Ankur (04/12/2009 - 14:09)
under the title "checking user is logged in" , it has been incorrectly mentioned that to access the user data from any controller, we need to use $this->Controller->_User. The right way to access the user data passed from app_controller is $this->_User
James (05/12/2009 - 00:31)
@Ankur: Very good point, thanks for letting me know.
Jeff (05/12/2009 - 20:22)
Thanks for your article(s) they have helped immensely. I'm having a bit of trouble. Everything is working fine, I log in, it tells me I've logged in successfully but then displays an error "Error: The requested address '/admin/posts' was not found on this server." I'm sure its something silly, any thoughts? Thanks.
James (06/12/2009 - 22:30)
@Jeff: Have you created a Posts Controller that has an "admin_index()" action in there. That's the only thing I can think of that would cause the error.
Jeff (07/12/2009 - 04:34)
UPDATE:
Yes, I had. The problem was I did not re-bake the admin controls in. I thought if I manually added everything and followed the tutorial I wouldn't have to run it. So I ran it and it fixed everything, must be a setting somewhere it updates that I didn't see.
Thanks for your help!
nitin (28/01/2010 - 20:30)
Hey James Great Tutorial. I am getting VALID_NOT_EMPTY - assumed 'VALID_NOT_EMPTY' error Please rectify me if i am doing wrong
James (01/02/2010 - 04:48)
@nitin: The "VALID_NOT_EMPTY" may not be valid in the latest version of Cake so you may have to try another validation rule instead or your own regular expression.
nitin (07/02/2010 - 20:39)
Hi James, I have removed "VALID_NOT_EMPTY" code in user.php. I am using latest version of cakephp 1.3 and it works fine.
Thanks a lot
alex (14/02/2010 - 15:15)
I've tried the solutions to the "blank" login page problem - but I'm still getting an empty page, I'm running 1.2.6 - any ideas?
James (14/02/2010 - 22:46)
@alex: Not too sure why you're getting a blank page, are the routes setup correctly? Try debugging your Controller to see if the code is being executed from there. Are your view setup correctly? A bit difficult to help you out really without more info.
alex (15/02/2010 - 15:13)
ok, so blank login appears to be bypassed - overlooked the line about the dashboard controller, now though with _no_ users in the db it considers me logged in straight away and takes me to the controller i specified.
any ideas?
alex (16/02/2010 - 02:35)
well the blank pages have now gone, i've now got a problem where - with no users in the database - i seem to be considered logged in automatically. should this be the case?
Jerry (16/02/2010 - 19:55)
Unable to get your code to work. When I enter my password, I always get the incorrect password message even though a password is stored in the database. I printed $this->data in the users controller login action to the screen and it always prints empty. Do you know what I am doing wrong here? I am fairly new to cakephp.
James (16/02/2010 - 22:38)
@alex: no that shouldn't be happening, have a look at the beforeFilter() method in the app_controller() that should be checking the session to see if the User data is present. What may be happening is that an old Session still exists. Try clearing the cache of your browser.
@Jerry: Not too sure what's going wrong, it's quite difficult to troubleshoot without seeing your code. Go over the article again and make sure you cover everything.
sooraj (22/02/2010 - 01:31)
Hi i have gone through your article, its quite informative. I am a new bee in cakephp. can you plz help me in clarifying certain doubts. is it possible to convert back the hashed password in case of password recovery.
James (22/02/2010 - 04:48)
@sooraj: no it's extremely difficult to recover a hashed password, especially in this case when a password salt has been used. Systems usually provide a recover password feature which simply resets the password or creates a random new one.
filmer (27/02/2010 - 01:26)
James, I've been trying to get this to work in conjunction with your first tutorial and for some reason I'm ALWAYS getting a blank page on EVERYTHING once I implement this. I would be willing to shoot you a copy of my code, but I did everything you said explicitly and still I'm coming up with a blank page.
This really is exactly what I need to get working. If you could help me figure this out I would greatly appreciate it. And my friend's who I'm making websites for would be happy too.
I've been trying to get this to work for over a week now and I can't seam to find out what is going wrong here.
James (27/02/2010 - 03:02)
@filmer: send me an email with your code and I'll try and have a look today or alternativly if it's online let me know the URL and maybe server access and I can have a look for you.
Mary (28/02/2010 - 07:46)
hi james thanks for this nice tutorial!
http://www.jamesfairhurst.co.uk/posts/view/creating_an_admin_section_with_cakephp_updated
i've found a problem and i'd like to know if you could help me.
in the user.php funciton:
check_login($data){
....
...
if($user['User']['password'] == md5($data['User']['password'])) {
$valid = $user;
}
return $valid;
}
always return me false and it's because of I 've been
writing directly the password to the database.
It will never match cause it's being compared with the
value of an encrypt function : md5..
you have any solution ? !
Michael (12/03/2010 - 14:44)
The setFlash can blank your page and cause apache segfaults if you don't have the class defined - which is really bad.
To fix use 'default' instead of 'flash_bad' and 'flash_good'.
Hope this helps.
bipul (15/03/2010 - 23:03)
After entering the form details i am getting the error
invalid sql syntax and while checking i m seeing the
$sql as "check_user_data"
James (17/03/2010 - 23:53)
@bipul: what SQL error is it, must be the actual find operation. Double check all your tables and that the fields are the same as my example.
Atea (01/04/2010 - 06:03)
Very nice quick fix for an admin section! Thank you!
neel (14/04/2010 - 00:30)
thanks for this great tutorial....
Michel (30/04/2010 - 14:59)
I've followed your tutorial, but sometime's i get the error:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 40961 bytes) in C:\xampp\htdocs\michelpost\cake\libs\view\view.php on line 662
Do you no what is causing this?
James (03/05/2010 - 05:31)
@Michel: that can be caused by a large number of different things, usually intensive processing can make that error so if you're using lots of for loops in your code. As a solution you could increase the allowed memory limit on your server or if you can't do that because you're on a shared hosting package you must find the culprit in your code and refactor it somehow so that it's not that resource intensive.
Usman (12/05/2010 - 09:57)
Hi, can i use this code for cakephp 1.3 the latest version?
Thanks,
James (12/05/2010 - 10:05)
@Usman: I've not tried it myself but let me know how you get on.
Usman (13/05/2010 - 13:31)
Hi James, it's working but had to do a few modifications, changed the parameters in setFlash and also had to move the check_user_data function from model to controller as for some reason it wasn't working before giving some sql error.
Thanks
Brandon (19/05/2010 - 09:09)
Tried this tutorial in cakephp 1.3. Had to make some mods to get it working. I had issues logging in because the validation rules in the users model weren't working. I couldn't even get the form to post correctly even if I submitted a valid username and password. Did some googling and found the following at this link http://teknoid.wordpress.com/2008/06/09/15-essential-cakephp-tips/
The old VALID_NOT_EMPTY constant is now deprecated and there does not seem to be a rule to replace it… Well, it’s easy enough by using: 'rule' => array('minLength', '1')
Martin Bavio pointed out that having space characters (only) as your field data will make the validation rule pass and this is probably not a desirable effect. Using this simple regex, instead, will catch an empty string with space characters: 'rule' => array('custom', '/\S+/')
Seems to be working now. Great tutorial! Very useful.
Jordan (13/06/2010 - 12:27)
I'm having the same problem that Mary (on of your commentors was having.
if($user['User']['password'] === md5($data['User']['password'])) {
$return = $user;
}
This always returns false. Do you have a solution?
James (24/06/2010 - 04:55)
@Jordan: Not too sure why its' doing that, if you're storing the passwords as plain text like Mary then just remove the md5 functions so it's comparing the correct values. Otherwise just add some debugging code to see what strings are actually being compared.
pinky (27/06/2010 - 22:26)
Hi
after login its displaying as "cannot modify header information".Help me plzzz...Am new in cake php
James (27/06/2010 - 22:30)
@pinky: The "cannot modify header information" error usually means that you've ouputted something to the screen before doing a redirect so uncomment all pr() statements and ensure that nothing it being echo'd to the screen.
rlcabral (13/07/2010 - 08:02)
Hi,
I understand that with the actual release (1.3.2) CakePHP supports multiple namespace besides 'admin'. Seems that the Routing.admin is now accepting an array that we can put whatever name we want.
Do you know if this tutorial will work well with Cake 1.3.2?
I have always followed the steps you teach here whenever I wanted to bake an admin area and it works fine. It even works with the workaround to allow multiple levels (which is what the new release is offering now). But I really don't know if it will work if I upgrade.
James (20/07/2010 - 11:12)
@rlcabral: Very good question, I don't know the answer to that one just yet, I've not had a look at the new release but I assume you would just have to remove the custom work around to get the multiple levels working correctly.
SimonJPA Baillargeon (22/07/2010 - 11:07)
Hi,
I'm using CakePHP 1.2.3.8166.
I did copy all the code that you provided. When I login and I don't use the right password it send me to a blank page.
When I use the right password it send me also to a blank page. No errors appears.
Regards,
James (24/07/2010 - 03:35)
@SimonJPA: Not sure what's going on there. Try enabling PHP errors on the server if they're not already enabled, it may shed some light. Otherwise see where the page is redirecting too.
CoderDJ412 (25/07/2010 - 21:02)
Does this work with CakePHP 1.3.2? If not, what edits do you have to make to make it work?
James (07/08/2010 - 01:50)
@CoderDJ412: I haven't tried it with the latest version of CakePHP but it should work. Let me know how you get on.
nitin (17/08/2010 - 22:12)
Hi James
I am big fan of yours and making the website in cakephp 1.3 and i need to use two validate method in one model e.g $vaidate1 , $validate2 because i have 2 forms having different validation Please reply on it
Thanks
atul sharma (21/08/2010 - 05:48)
hi, thanx for this post it helped me a lot in understanding the concepts of creating a session in cakephp. But this method of yours is not working properly in cake 1.3.3.
Arpana (31/08/2010 - 05:58)
how can i use this with CakePHP 1.3.3?
James (01/09/2010 - 22:46)
@nitin: Good question, I usually just unset the unwanted validation fields from the Controller e.g:
unset($this->User->validates['rule']);
But I'm thinking there's a better way to do this which I'll have a search for.
James (01/09/2010 - 22:49)
@Arpana: Just finishing a post of using this with 1.3.3 and also providing the entire code as an example. Stay tuned!