Need Help With User Identity

Hello everyone! I’m working on my first Yii project and have run into my first major problem. I’m using the advanced template for my project (in case this makes a difference at all).

I know that the framework already has signup and login functionality built in, however, I want to modify it a bit for my purposes. I’m trying to implement an email confirmation step. This by itself I’ve been able to figure out but I’m trying to think long term and database management. So here’s what I’m doing/thinking.

I’ve created 2 user tables: user and pend_user. When a person signs up, their information is placed in the “pend_user” table. Once they confirm their email, their information is removed from “pend_user” and added to “user”. What I want to happen is that, when they log in BEFORE confirming their email, they’re taken to a page that tells them they must confirm their email. The problem arises because Yii only looks in the “user” table for user information.

Now, at this point, you’re probably asking, why am I using 2 separate tables for pending members and activated members? The reason is because I don’t want to maintain accounts that haven’t been activated. Ultimately, I’d like the “pend_user” table to be purged on a regular basis. This would undoubtedly be faster than running a scheduler that routinely scans the entire “user” table and deletes accounts who haven’t confirmed their email in a certain amount of time. A simple purge of an entire table would obviously be much faster.

Having said all that, how can I teach/modify Yii to look into both tables for user identity? Or is there another, simpler way to do this that I’m not thinking about?

Also, I’m very new to Yii and programming in general, so please let me know if what I’m asking for here is overly complicated to achieve.

Thanks.

bad design!

You only need single table with single field "is_activated" for testing activation and "activation_token" for building activation url/activation validation

To avoid processing pending user check for "is_activated" and process acordingly!

Hi Stefano,

Thanks for the advice, but I’m already aware of that methodology. Further, I’ve outlined my reasons up above as to why I want to avoid it. If it is indeed a “bad design”, please offer up your reasons after reading in detail what I’ve written.

Hello there, QuPsi. Great that you decided to start using Yii, you won’t regret it.

Although I still have not had the pleasure of working with Yii2, I have used Yii reasonably, now, to business (a TL;DR is provided at the end of the post with the solution per sé, in case you want to skip all the process to get there):

First off, let’s see the controller, SiteController::actionLogin contains the following code:




$model = new LoginForm();

if ($model->load(Yii::$app->request->post()) && $model->login()) {

    return $this->goBack();

    ...



OK, so we have a LoginForm model, whose login() method is called, let’s check that part,




public function login()

    {

        if ($this->validate()) {

            return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);

        } else {

            return false;

        }

    }



So, LoginForm::validate() is called and, if it’s successful, the user is logged in, fair enough. Let’s check LoginForm::rules real quick:




public function rules()

{

    return [

        // username and password are both required

        [['username', 'password'], 'required'],

        // rememberMe must be a boolean value

        ['rememberMe', 'boolean'],

         // password is validated by validatePassword()

        ['password', 'validatePassword'],

     ];

}



Luckily, lots of comments in here; the first rule checks that there is a username and a password, the second one checks that rememberMe has no funny values and the third rule… well, I don’t know, let’s check LoginForm::validatePassword()!:




public function validatePassword()

{

    if (!$this->hasErrors()) {

        $user = $this->getUser();

        if (!$user || !$user->validatePassword($this->password)) {

            $this->addError('password', 'Incorrect username or password.');

        }

    }

}



Aha!, after checking that the previous rules added no errors, a call to LoginForm::getUser() is the first thing to catch our attention, just for the record, let’s see what it says:




public function getUser()

{

    if ($this->_user === false) {

        $this->_user = User::findByUsername($this->username);

    }


    return $this->_user;

}



Alright, now the solution:

TL;DR: You can alter LoginForm::validatePassword and add the alternate case when the user is not in the user table, but is in the pending users table, something like this would suffice here:




if (!$this->hasErrors()) {

    $user = $this->getUser();

    if (!($user && $user->validatePassword($this->password)))

        $user = $this->getPendingUser();

        if (!($user && $user->validatePassword($this->password))) {

            $this->addError('password', 'Incorrect username or password.');

        }

    }

}

...

public function getPendingUser()

{

    if ($this->_user === false) {

        $this->_user = PendingUser::findByUsername($this->username);

    }


    return $this->_user;

}



Of course, this implies that PendingUser is pretty much the same as User, maybe you want to extend user and override the tableName() method (notice that this may not suffice, I recommend implementing IdentityInterface and doing things the way you’re more comfortable).

Now, for the controller, you may want to add a flag to your customized LoginForm, something like LoginForm::getIsPendingUser() (implementation is up to you) and use it in your controller to achieve the desired effect which, I think, was a redirection, like so:




if ($model->load(Yii::$app->request->post()) && $model->login()) {

    if($model->getIsPendingUser()) {

        $this->redirectoToDoSomething();

    } else { 

        $this->doSomethingElse();

    }



Also take note that, if you want to make that “pending” status persistent through the application (i.e., you’ll use it for something else later), you may want to look into storing it in your user object, but I’ll leave that one to you.

Good luck, and have fun coding!

Hey Anesed,

Thank you for the great writeup! I really appreciate you taking the time to write it out! What you’ve described is ALMOST exactly what I’ve done so far. The only “issue” is that, after the redirect, the header will show the user logged in (shows “logout {user}”), but the moment you navigate to any other page, it logs the user out (header changes to back to “signup” and “login”).

What I was hoping to achieve was, as long as the user is in the pending table, they can log in, but anything they click on will redirect to the page saying they need to confirm their email address. Part of the reason for this is because I want to include a link to allow them to have the email resent. If they navigate away from the page, they’ll have to login again in order to reach that link, should they need it.

I’m not sure how to achieve the persistent logged in and redirect because with each request, the application only checks the “user” table to see if the user is logged in or not.

The more I think about it, the more I’m leaning that the way things are is ok, however, I’m still curious on how to achieve what I’ve described, even if it’s just for education/knowledge purposes.

Thanks again! Now that I’m really starting to get this, it really is becoming more enjoyable and fun (I hated coding in undergrad LOL).

No problem, mate. For the redirect part, I think a custom access rule is a reasonable solution, in your controllers, just add a custom rule to your access behavior, like so:




public function behaviors()

{

    return [

        'access' => [

            'class' => AccessControl::className(),

            'only' => [/* Actions*/],

            'rules' => [

                [

                    //Notice, no 'action' property since we want to catch everything

		    'allow' => false,

		    'matchCallback' => function($rule, $action) {

			return Yii::$app->user->isPending; //For Example, it tells you if the user is pending activation

		    },

	            'denyCallback' => function($rule, $action) {

                        return Yii::$app->user->goWhereYouCanActivateYourAccountMate(); // Once again, for example

                    }	

		],

            //Other rules

            ]

        //And so forth

        ]



Look out for the ‘only’ and ‘except’ properties of your ‘access’ behavior, so you don’t find that, surprisingly, the user can go and navigate somewhere he should not.

Now, with the user not psersisting… that seems a bit odd, my user persists normally in the advanced app (although mine is in mint condition), check for some odd configurations in your user component or unnoticed calls to User::logout(). A good starting point would be to check for changes in your browser cookies (there is a ‘_identity’ cookie set when you’re logged in, usually).

Hope this helps you at all. As I said before, I still haven’t worked with Yii2, so sorry if my answers are somewhat off [excuses]but the best learning experience is through trial and error![/excuses].

Cheers.

Ah, I’ll look into the access control. The only issue is this:




Yii::$app->user->whateverYouWant



Yii is "hardcoded" to look into the "user" table when using this line and thus is the source of my issue. Given that the pending user is not in the "user" table et, the query result will always be null when Yii looks to see if the user is in the "user" table, thus logging the user out on the next command. This is why I was wondering how involved it would be to modify Yii to look into both tables.

Like I said, I’m leaning towards leaving it this way (might annoy the user, but hey, they’ll be more inclined to confirm their email right? lol), but I’m curious to know how to achieve what I’ve described above for knowledge sake.

Ok so after doing A LOT of digging, I’ve found the “problem” and have come up with a solution (I think).

Yii essentially only uses one table for identifying a user (depending on frontend or backend). As I’ve explained up above, I want to separate this into 2 (and may add a third for admin only in the backend, haven’t decided yet). The table that Yii uses is set in the components section by setting “identityClass” in /frontend/config/main.php file (this took me HOURS to find, eventually had to search the entire project (should’ve did that first… smh), still figuring out where everything is).

Anyway, so now that I’ve figured out how this works, here is my proposed solution that I’m going to try (will try tomorrow, my mind is mush from trying to figure this out tonight).

I’m going to make another yii2/web/user.php file and call it pendUser.php. Then, I’m going to “hardcode” the identityClass to be “pendUser” or have it call “identityClassPend” and set it in main.php (haven’t decided yet, gonna sleep on it). Then, once this is done, whenever I want to check in the pend_user table, I SHOULD be able to call


Yii::$app->pendUser

If this works, then I should be able to make a forced redirect for any user that is logged in from the “pend_table”. I’ll need to look up how to do this, cause as of right now, I don’t know how.

I don’t know if this will work, but I will find out tomorrow when I implement it. I’ll post back the results.

why on earth you would waste time because of such reason? How long does user remain pending before activated and how does that affect anything in your database (I guess you are concerned of performance and space here)? AFAICS, there is no impact at all even if one activates his account after a week unless the whole world is bogging your site with in-activated accounts.

So I don’t see your point of doing that at all (You can go ahead and just do that though). If you are concerned of people taking a year or so to activate then tell them if they don’t activate say in three months account will be purged and so do run purging script for accounts not activated in three months. That makes more sense to me than what you are doing.

Like I said, I’m thinking long term here. Here’s what I’m trying to avoid…

That user list is going to be used for quite a few operations, as it grows, I want to keep it lean.

Before you go in there, take these:

http://www.yiiframework.com/doc-2.0/guide-authentication.html

http://www.yiiframework.com/doc-2.0/yii-web-identityinterface.html

http://www.yiiframework.com/doc-2.0/yii-web-user.html

And remember good ol’ Ben Kenobi’s words: “Use the docs, Luke”.

I’ll play devil’s advocate here.

What you’re doing is unnecessarily complex and not needed. If you ever get big as facebook, google, or twitter maybe, then sure, it might be useful when you have m/billions of users. But the other 99.99%+ of the time? Complete waste of time.

In short, you’re prematurely optimizing your app for no reason at the cost of time and complexity. It’s nice that you’re looking ahead and trying to prevent problems in the future, but you need to figure out the proper balance. This is a common problem for new developers, and even in that article you linked the author says “Frankly, many of them aren’t really a problem in reality.

Stick with the "is_activated" field and call it a day. Spend that time learning other useful things.

Edit: Here’s some useful data from a test I ran:

i5-3317u 1.7ghz / 4gb RAM

tbl_product_test = 1,039,479 records / MyISAM / utf8_general_ci / 1.3 GiB

delete FROM tbl_product_test where affiliate_id=1

207870 rows deleted. (Query took 61.5566 sec)

First, you’ll have a better computer server to run this. Second, you can run this query every few hours such that you won’t need to delete that many records at once (maybe a few thousand at most).

So you’re looking to run a query every few hours that will take mere seconds. This is not worth optimizing.

Amnah,

Thank you for that, especially the numbers at the bottom. That really puts things into perspective. I will keep the single user table in the final project.

Still, I may try my idea to see if it works at all. I know it’s unnecessary, but it’ll further my understanding and mastery of Yii. Considering I’m very new to this, it’ll be an exploratory exercise for me.

Well I think you should not modify it, as it will further give you problems in login creations