A form takes in an email and pw
<?= $this->Form->create() ?>
<?= $this->Form->control('email') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>
The email is stored as an ID in Users, and Password is in password table
Address is the attribute in Emails table that stores the actual email address
Password is where pw is stores
The authenticate component takes in address - which
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => [
//here we define what is compared to be authenticated
'username' => 'address',
'password' => 'password'
]
]...
The login function is like normal:
public function login()
{
if ($this->request->is('post')) {
//PUT IN STUFF HERE
$user = $this->Auth->identify();
if ($user) {
$user->last_login = Time::now();//dont put this above uif statement, will automatically create a default object
$this->Auth->setUser($user);
$this->Flash->success('You have been successfully logged in.');
$this->log("Login success", 'debug');
//redirect after login
return $this->redirect($this->Auth->redirectUrl('/users/index'));
}
$this->Flash->error('Your username or password is incorrect.');
$this->log("Login FAILURE", 'debug');
}
}`
How i see it, we either compare email id's or get the form to look directly at the Associated classes 'address' attribute. How does one point the authentication to the attribute in another table like that
Thanks
You have to create Custom Authentication Objects for that
On load component
$this->loadComponent('Auth', [
'authenticate' => [
'CustomForm' => [
'fields' => [
'username' => 'address',// Field in your emails table
'password' => 'password',// Field in your users table
'myAssoc'=>'Users'// Custom Filed to get association
],
'userModel' => 'Emails'
]...
Create a file CustomFormAuthenticate.php in /src/Auth/ folder
<?php
namespace App\Auth;
use Cake\Auth\FormAuthenticate;
use Cake\Utility\Inflector;
class CustomFormAuthenticate extends FormAuthenticate
{
public function _findUser($username, $password = null)
{
$result = $this->_query($username);
$myAssoc = false;
if (!empty($this->_config['fields']['myAssoc'])) {
$myAssoc = $this->_config['fields']['myAssoc'];
$result->contain([$myAssoc]);
}
$result = $result->first();
if (empty($result)) {
return false;
}
if ($password !== null) {
$hasher = $this->passwordHasher();
if($myAssoc !== false){
$hashedPassword = $result->{Inflector::underscore(Inflector::singularize($myAssoc))}[$this->_config['fields']['password']];
} else {
$hashedPassword = $result->get($this->_config['fields']['password']);
}
if (!$hasher->check($password, $hashedPassword)) {
return false;
}
$this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);
$result->unsetProperty($this->_config['fields']['password']);
}
debug($result);
return $result->toArray();
}
}
Make sure you have association of model Users with Email in your EmailTable.php
$this->hasOne('Users', [
'foreignKey' => 'email_id'
]);
In your login page
<?= $this->Form->create() ?>
<?= $this->Form->control('address') ?> // Field in your email table
<?= $this->Form->control('password') ?>// Field in your users table
<?= $this->Form->button('Login') ?>
<?= $this->Form->end() ?>
I have tested it and its working for me.
I would suggest a less intrusive way, that is, using a custom finder that contains/joins the users table, and sets the password field on the main query using an alias, or on the main entity as a virtual field, that way the built-in authenticator retrieves the data it needs, which is all that matters for the authenticator.
For example in your EmailsTable class, add a finder like this, which selects the proper value for the password field:
public function findAuth(\Cake\ORM\Query $query, array $options)
{
return
$this
->find()
->select([
'Emails.id',
'Emails.address', // you may want to alias this one too
'password' => 'Users.password'
])
->leftJoinWith('Users')
->where([
// the options is always named `username`, this is
// not affected by the `fields` configuration
'Emails.address' => $options['username']
]);
}
With such a finder, all you then need to do is to configure the fields, userModel, and finder options for the auth component, like:
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => [
// set the field to `email`, just like in your form
'username' => 'email'
],
'userModel' => 'Emails',
'finder' => 'auth'
]
// ...
]
]);
This example assumes that Emails is associated with Users via a belongsTo or hasOne association, using the join strategy.
Also note that the username field is set to email just like in your example form, you could as well set both to address (or anything you like actually), it will not affect the finders query, as it creates a new query, and uses the username value that was extracted from the request data via the configured field (the extracted value will always be passed in the username key of the $options array, unless the finder configuration would be an array that already has a key named username).
See also
Cookbook > Controllers > Components > Authentication > Customizing Find Query
Related
First of all I love the way that validation is going through, can now easily use
public function authorize(Authenticator $auth)
{
return $auth->user()->hasRole('administrator');
}
hat's not the problem, I bump always into another problem... that is when you update an record, how to do things with the rules? If I need to update an email, I need the following string: 'email' => 'unique:users,email_address,10'. In this case it should look like:
public function rules()
{
return [
'email' => 'required|unique:users,id,?????',
'tags' => 'required'
];
}
It's more simple.
The Laravel documentation says "If your table uses a primary key column name other than id, you may specify it as the fourth parameter":
'email' => 'unique:users,email_address,'.$user->id.',user_id'
If for example, you want to verify if a username exists, but excluding current user ID:
// UpdateUserRequest.php
public function rules() {
//
return [
'username' => 'required|unique:users,username,' . $this->id . ',id',
];
}
I'm following the Definitive Guide to Yii 2.0. In my application I have two roles: the admin, who can do everything and the viewer, who can do some actions that unregistered users can't do. I'm trying to use default roles functionality of Yii 2 RBAC, but it seems doesn't work. The user table in my database has a column named "role": for admin it's value set to 1 and for viewers = 2.
What I did:
/app/rbac/UserGroupRule.php
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
class UserGroupRule extends Rule {
public $name = 'userGroup';
public function execute($user, $item, $params) {
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->role;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'viewer') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$author = $auth->createRole('viewer');
$author->ruleName = $rule->name;
$auth->add($viewer);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $viewer);
in my controller:
public function behaviors() {
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['admin'],
'rules' => [
[
'allow' => true,
'actions' => ['admin'],
'roles' => ['admin'],
],
],
],
];
}
When I try to access "admin" action, it says Forbidden #403, even when I'm an admin. How to make it work?
The user table in my database has a column named "role": for admin it's value set to 1 and for viewers = 2
That's not how it works unfortunately.
The rights/roles a user has are (by default) done via the auth_assignment-table.
Just add an entry in it:
INSERT INTO `auth_assignment` VALUES ("admin", <user-id>, NOW());
(be sure to change the user ID into whatever user you want to make admin.
That should solve your issue.
Edit (as I misread some of your question):
As per this link you can indeed define default roles, but you have to make sure to also reconfigure your authManager-component in the configuration file to include the default roles:
'components' => [
'authManager' => [
// ...
'defaultRoles' => ['admin', 'viewer'],
],
],
This list of roles indicate the permissions that always should be checked for every user, no matter if they have an entry in the auth_assignment-table or not.
I was facing the same issue with op. Finally made it work after tinkering with xdebug for a while.
I feel the official documentation on default roles is missing a couple important points, I will summarize them below with some of my personal experiences. The project structure is based on Yii 2.0 Advanced Project Template
Database
user table contains id and group. Where group is type int, 1 for admin and 2 for author
Rules setup
Code simplified for clarity.
The rule class, where you put the actual rule logic.
yii/console/controller/UserGroupRule.php
namespace app\rbac;
use Yii;
use yii\rbac\Rule;
/**
* Checks if user group matches
*/
class UserGroupRule extends Rule
{
public $name = 'userGroup';
public function execute($user, $item, $params)
{
if (!Yii::$app->user->isGuest) {
$group = Yii::$app->user->identity->group;
if ($item->name === 'admin') {
return $group == 1;
} elseif ($item->name === 'author') {
return $group == 1 || $group == 2;
}
}
return false;
}
}
Now defining the roles..
yii/console/controller/RbacController.php
namespace console\controllers;
use Yii;
use yii\console\Controller;
class RbacController extends Controller
{
public function actionInit()
{
$auth = Yii::$app->authManager;
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);
$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
// define 'author' here...
}
}
After you have this file ready, you should be able to run ./yii rbac/init to generate the rule files:
console/rbac/items.php
console/rbac/rules.php
Important: You need to place the generated files under your desired application folder, this is crucial. Other wise Yii 2.0 will not be able to pick up the rules. For example: yii/backend/rbac/
Controller and config setup
This is mostly identical to the documentation
yii/commom/config/main.php
Add the following to the return array:
'authManager' => [
'class' => 'yii\rbac\PhpManager',
'defaultRoles' => ['admin', 'author'], // your define roles
],
Now the fun part, under the controller class you would like to apply the rules
yii/backend/controllers/SiteController.php
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => [], // applies to all actions
'roles' => ['admin'], // your defined roles
],
],
],
Up to this point, the rules should be working. Under your controller class, double check Yii::$app->getAuthManager() see if it contains your defined roles. If not, it means Yii did not pick up the rules correctly, please check previous steps again.
I am trying to validate an update user profile form, whereby the validation should check that the email doesn't exist already, but disregard if the users existing email remains.
However, this continues to return validation error message 'This email has already been taken'.
I'm really unsure where I'm going wrong. Otherwise, the update form works and updates perfectly.
HTML
{{ Form::text('email', Input::old('email', $user->email), array('id' => 'email', 'placeholder' => 'email', 'class' => 'form-control')) }}
Route
Route::post('users/edit/{user}', array('before' => 'admin', 'uses' => 'UserController#update'));
User Model
'email' => 'unique:users,email,{{{ $id }}}'
Your rule is written correctly in order to ignore a specific id, however, you'll need to update the value of {{{ $id }}} in your unique rule before attempting the validation.
I'm not necessarily a big fan of this method, but assuming your rules are a static attribute on the User object, you can create a static method that will hydrate and return the rules with the correct values.
class User extends Eloquent {
public static $rules = array(
'email' => 'unique:users,email,%1$s'
);
public static function getRules($id = 'NULL') {
$rules = self::$rules;
$rules['email'] = sprintf($rules['email'], $id);
return $rules;
}
}
You can accomplish this with the sometimes function of the validator
Something like:
$validator->sometimes('email', 'unique:users,email', function ($input) {
return $input->email == Input::get('email');
});
See http://laravel.com/docs/4.2/validation#conditionally-adding-rules for more info
I'm using a validation service to validate user submitted form input (something along the lines of: http://laravel.io/bin/vrk).
Using this approach (validation service classes) to validate user submitted form data against a set of rules, how can I validate user submitted data when rules have a unique rule. For example, if a user has the username of John then when I try to update the model validation fails (because John exists as a username, even though it belongs to the current model).
To solve this in Laravel I can do something like 'username' => 'required|alpha_dash|unique:users,username'.$id. How should I modify my current code, in the link, to best accommodate this? Should I have separate validator classes depending on the scenario (for example, UserCreateValidator, UserUpdateValidator, etc). Or should I do something like create separate validation rules in UserValidator class and pass which rule I want as an argument to either the constructor or the passes() method when calling UserValidator?
I think you could do something like this
First update UserValidator rules like this.
class UserValidator extends Validator {
// Override parent class $rules
protected $rules = [
'default' => [
'username' => 'required|alpha_dash|unique:users',
'password' => 'required|between:6,16|confirmed',
'password_confirmation' => 'required|between:6,16'
],
'update' => [
'username' => null,
]
];
}
Then modify Validator's passes method like this
public function passes($rule = null) {
$rules = $this->rules['default'];
if ($rule && isset($this->rules[$rule])) {
$rules = array_merge($rules, $this->rules[$rule]);
}
$validator = \Validator::make($input, $rules);
if ($validator->fails()) {
$this->validator = $validator;
return false;
}
return true;
}
Then in your controller's PUT method, this will merge update rules to default rules
$rule = 'update';
// user has changed his username
if ($input['username'] !== $old_username) {
$rule = 'create'; // validate uniqueness
}
else {
unset($input['username']); // remove it, we don't validate it anymore since it's the same
}
$validator->passes($rule); // override 'default' rules with 'update' rules
You don't have to change your controller's POST method, it'll stay the same
$validator->passes(); // use 'default' rules
If I'm understanding right, you have issues updateng data because of primary key constraints on your model. What you need to do is to create 2 sets of rules, one for insert, and one for update.
Asuming you have a set of rules like this:
protected $rules = [
'id' => 'required|unique:users'
]
You should implement something like this:
protected $rules = [
'id' => 'required|unique|unique:users,id,' . $this->id
];
This should tell laravel to ignore the duplicate id in the table users for the specified id, in this case, the id for the current object.
You can read more about this on laravel's documentation at http://laravel.com/docs/validation
unique:table,column,except,idColumn
The field under validation must be unique on a given database table.
If the column option is not specified, the field name will be used.
Well, what are you doing on post?
Because this is what you should be doing:
$user = User::find($userId);
$user->username = $input['username'];
$user->email = $input['email'];
$user->save();
To update a record.
Or
$input = array('username' => 'w0rldart', 'email' => 'hahafu#dumbledore.com');
// Retrieve the user by the attributes, or create it if it doesn't exist,
// based on the data above, which can come from an Input::all();
$user = User::firstOrCreate($input);
... many possibilities. But you could also do:
$input = array_forget($input, 'username');
To comply with your case, by removing the username index from the input array.
This is all I call tell you, based on the information you gave us. If you want more, post the controller's put method.
Update:
Here's my version of your PUT method: http://laravel.io/bin/OaX
I really think that try catch syntax is useless, since it's obvious that a User model will always be there. But I still don't know what you're trying to update. Even though I can't test it right now, I don't think that updating should be giving that problem, and if it does, retrieve user by username/id then unset the username index in your input array, and update it according to your specifications.
A little modification in UserValidator class
class UserValidator extends Validator {
// Override parent class $rules
protected $rules = [
'username' => 'required|alpha_dash|unique:users',
'password' => 'required|between:6,16|confirmed',
'password_confirmation' => 'required|between:6,16'
];
// ADD THIS
public function __construct(Array $rules = array())
{
parent::__construct();
if(count($rules)){
foreach($rules as $k => $v) $this->rules[$k] = $v;
}
}
}
In your controller putUpdate method
$user = User::whereUsername($username)->firstOrFail();
$rules = ['username' => 'required|alpha_dash|unique:users,username,'. $user->id];
// Pass the rule to update the rule for username in this method
$validator = \Services\Validators\UserValidator(Input::all(), $rules);
Check the manual here.
I'm little confused about controller and model in MVC framework (codeIgniter). Its clear to me that controller methods calls the views and Model methods interact with database.
However, I'm little confused about the following types of methods, which are called by methods in a controller.
hash_password //returns hash password.
valid_email //validates email format and return true or false
is_logged //check if session has a variable, returns true or false
generate_random_string //generates and hashes a random string
Should they be placed in controller or in a model?
Currently I place all of the above functions in a controller. Is it correct?
I think the is_logged should be placed in the Model for User. Note that the User might be a customer in your case or any class that you have made to model a user of your service.
The valid_email and generate_random_string are more or less utility functions, which you can place in a Utility or Utilities model, so that these are reusable in various controllers in your application.
The hash_password, can be placed in either the User model or Utility model. I am more tempted to place it in Utility model, since its a hashing function and there is nothing the user cares about. However, I can imagine there can be argument(s) otherwise.
The following SO question (though for a different framework) can also serve as a rule of thumb:
Where to put custom functions in Zend Framework 1.10
generally controllers are used to determine how to handle the http requests made..
There's nothing wrong in creating some functions which directly respond to the http requests.
but if it has anything to do with the DB, its better to place those function in the model, and call them from the controller.
Controller should combine view with model, so every validation shoulde be placed in model
this is my example from kohana
CONTROLLER
<?php
/**
* User Controller
*/
class Controller_Admin_User extends Controller_Admin_Template {
public function action_index()
{
$this->template->body = View::factory('admin/user/index')
->set('i', 0)
->bind('users', $users)
->bind('groups', $groups)
->bind('id_user_group', $id_user_group);
$model_user = new Model_Admin_User;
$users = $model_user->get_users(Arr::get($_GET, 'sort'), Arr::get($_GET, 'order'));
$model_usergroup = new Model_Admin_Usergroup;
$groups = $model_usergroup->get_user_group();
}
public function action_add()
{
$this->template->body = View::factory('admin/user/form_add')
->bind('error', $error)
->bind('groups', $groups)
->bind('post', $post);
$model_usergroup = new Model_Admin_Usergroup;
$groups = $model_usergroup->get_user_group();
if($_POST)
{
$model_user = new Model_Admin_User;
if($model_user->save($_POST) == false)
{
$error = $model_user->error;
$post = $_POST;
}
else
{
$this->request->redirect('admin/user');
}
}
}
MODEL
class Model_Back_User extends Model {
private $qb;
public $aliases = array(
'id'=> 'id_user'
);
public $error = array(
'name' => null,
'surname' => null,
'login' => null,
'password' => null,
'id_user_group' => null,
'old_password' => null,
'new_password' => null,
'confirm' => null,
'email' => null,
'phone' => null,
);
private $rules = array(
'name' => array('not_empty' => null, 'alpha' => null),
'surname' => array('not_empty' => null, 'alpha' => null),
'login' => array('not_empty' => null),
'password' => array('not_empty' => null),
'id_user_group' => array('not_empty' => null),
'email' => array('not_empty' => null, 'email' => null),
'phone' => array('not_empty' => null),
'old_password' => array('not_empty' => null),
'new_password' => array('not_empty' => null),
'confirm' => array('matches' => array('new_password'))
);
public function __construct()
{
$this->qb = new Querybuilder;
//parent::__construct();
}
public function change_password($data)
{
$validate = Validate::factory($data)
->filter(true, 'trim')
->rules('old_password', $this->rules['old_password'])
->rules('new_password', $this->rules['new_password'])
->rules('confirm', $this->rules['confirm'])
->callback('old_password', array($this, 'password_exists'), array('id_user'=> $data['id_user']));
if($validate->check() == false)
{
$this->error = array_merge($this->error, $validate->errors('user'));
return false;
}
$u = Session::instance()->get('user');
$this->edit(array('password'=> $this->password($data['new_password'])), array('id_user'=> $u['id_user']));
return true;
}
public function password_exists(Validate $valid, $field, $param)
{
if($this->user_exists(array('password'=> $this->password($valid[$field]), 'id_user'=> $param['id_user'])) == false)
{
$valid->error($field, 'old password is incorrect', array($valid[$field]));
}
}
public function save($data)
{
$validate = Validate::factory($data)
->filter(true, 'trim')
->rules('name', $this->rules['name'])
->rules('surname', $this->rules['surname'])
->rules('user_group_id', $this->rules['id_user_group'])
->rules('email', $this->rules['email'])
->rules('phone', $this->rules['phone']);
$edit = false;
if(isset($data['id_user']) AND Validate::not_empty($data['id_user']))
{
$edit = true;
}
else
{
$validate->rules('login', $this->rules['login'])
->rules('password', $this->rules['password']);
}
if($validate->check() == false)
{
$this->error = array_merge($this->error, $validate->errors('user'));
return false;
}
if($edit == true)
{
$this->edit(
array(
'name' => $data['name'],
'user_group_id' => $data['user_group_id']
),
array(
'id_user'=> $data['id_user']
)
);
return true;
}
return $this->add(
array(
'name' => $data['name'],
'login' => $data['login'],
'password' => $data['password'],
'user_group_id' => $data['user_group_id']
)
);
}
protected function add($data)
{
$data['password'] = $this->password($data['password']);
return $this->_db->query(Database::INSERT,
$this->qb->insert('user')->set($data)->build_query()
);
}
View is not so important thats why i dont put this here.
Generally speaking - a model should know stuff about it's own data. So anything related purely to a model's own data - should go in the model.
Eg the hash_password and email-validation methods - a model should know how to validate or update it's own data-fields, so those should go in the model.
However a controller should know about how to direct user actions appropriately and to load the correct models for views etc.
EG the session-related method should go in the controller, because the session is used for storing the user's state (based on past actions).
The "generate random string" method is very vague and may be used everywhere. I'd put that in a separate library possibly included in the model/controller as appropriate.
I've been using Codeigniter for a long time and I'd do the following with your functions as far as placement goes:
hash_password //returns hash password.
I'd put something like a password hasher in a library or helper file so I could call it from my controller like:
// pretend library I'd make for tasks like hashing etc
$this->load->library('passwords');
// transform posted password into it's hashed version
$password = $this->password_library->hash_password($this->input->post('password'));
I'm assuming you want to hash/salt the password and store it in your database in that example
valid_email //validates email format and return true or false
This is already in form_validation, so...
is_logged //check if session has a variable, returns true or false
This should also connect to a authentication library
generate_random_string //generates and hashes a random string
Again, this would come from a library or helper.
SO WHEN DO YOU USE A MODEL?
Me, I use models exclusively for in/out on the database. All my queries go in there. I usually have my model's functions return data objects so I can loop through them in my views.
Controllers call your data from your models, then dump everything into your views. Outside functionality always goes into libraries and helpers. I like to do the "MY_library" and extend Codeigniter's own stuff - especially with forms and the html helper etc.