Jump to content
thetuningspoon

Integrating a member / visitor login form

Recommended Posts

I'm looking into developing a site with a "members-only" section in the front-end. I've been playing around with creating new users in the back end and then using the API to show certain content only to members logged in with certain roles, etc. It's really fantastic and I love how flexible it is.

Now I'm at the point where I'd like to provide a form which displays to visitors who are not logged in and allows them to do so (obviously I don't want to have to direct users to the default admin login page to log in). However, I have no idea how to do this... I tried looking at the code for the admin login page to see if I could borrow from that, but I can't even figure out where the code for the form is!

Any help would be much appreciated, as always :)

Share this post


Link to post
Share on other sites

I agree with nik, that that is useful. But wire('session')->login activates more things and somehow I think it's good to get respond back from PW.

SessionLoginThrottle for example, throttles the time up to 300 seconds,after that you're be able tp login again.

Actually I'm trying to write a module for frontend members.

covering:

  • 'changePassword',
  • 'confirmRegister',
  • 'forgotPassword',
  • 'login',
  • 'logout',
  • 'profile',
  • 'register',
  • 'resetPassword',
  • 'updateProfile'

I'm kinda stuck on the "login" due the lack of OOP knowledge. I don't know how to retrieve the data back from for instance "SessionLoginThrottle.module" or any other classes. Figured out SELF:: for example, but don't know where PARENT:: references to for example....

Realy have no clue, maybee I even start wrong :-)

class FrontMembers extends WireData implements Module, ConfigurableModule {
}

I'll continue fighting this week...

  • Like 2

Share this post


Link to post
Share on other sites

The custom login nik linked above works great for exactly what you are trying to do. The version I posted is essentially just a small expansion on Ryan's original.

I have further extended it to include password reset. I can post that code if you need it.

Share this post


Link to post
Share on other sites

I recently had to setup front-end system to handle logins, password resets and changing passwords, so here's about how it was done. This should be functional code, but consider it pseudocode as you may need to make minor adjustments here and there. Please let me know if anything that doesn't compile and I'll correct it here.

The template approach used here is the one I most often use, which is that the templates may generate output, but not echo it. Instead, they stuff any generated output into a variable ($page->body in this case). Then the main.php template is included at the end, and it handles sending the output. This 'main' template approach is preferable to separate head/foot includes when dealing with login stuff, because we can start sessions and do redirects before any output is actually sent. For a simple example of a main template, see the end of this post.

1. In Admin > Setup > Fields, create a new text field called 'tmp_pass' and add it to the 'user' template. This will enable us to keep track of a temporary, randomly generated password for the user, when they request a password reset.

2a. Create a new template file called reset-pass.php that has the following:

/site/templates/reset-pass.php

$showForm = true; 
$email = $sanitizer->email($input->post->email);
if($email) {
 $u = $users->get("email=$email"); 
 if($u->id) {
   // generate a random, temporary password
   $pass = '';
   $chars = 'abcdefghjkmnopqrstuvwxyz23456789'; // add more as you see fit
   $length = mt_rand(9,12); // password between 9 and 12 characters
   for($n = 0; $n < $length; $n++) $pass .= $chars[mt_rand(0, strlen($chars)-1)];
   $u->of(false);
   $u->tmp_pass = $pass; // populate a temporary pass to their profile
   $u->save();
   $u->of(true); 
   $message = "Your temporary password on our web site is: $pass\n";
   $message .= "Please change it after you login.";
   mail($u->email, "Password reset", $message, "From: noreply@{$config->httpHost}"); 
   $page->body = "<p>An email has been dispatched to you with further instructions.</p>";
   $showForm = false;
 } else {
   $page->body = "<p>Sorry, account doesn't exist or doesn't have an email.</p>";
 }
}

if($showForm) $page->body .= "
 <h2>Reset your password</h2>
 <form action='./' method='post'>
 <label>E-Mail <input type='email' name='email'></label>
 <input type='submit'>
 </form>
";

// include the main HTML/markup template that outputs at least $page->body in an HTML document
include('./main.php'); 

2b. Create a page called /reset-pass/ that uses the above template.

3a. Create a login.php template. This is identical to other examples you may have seen, but with one major difference: it supports our password reset capability, where the user may login with a temporary password, when present. When successfully logging in with tmp_pass, the real password is changed to tmp_pass. Upon any successful authentication tmp_pass is cleared out for security.

/site/templates/login.php

if($user->isLoggedin()) $session->redirect('/profile/'); 
if($input->post->username && $input->post->pass) {
 $username = $sanitizer->username($input->post->username); 
 $pass = $input->post->pass; 
 $u = $users->get($username); 
 if($u->id && $u->tmp_pass && $u->tmp_pass === $pass) {
   // user logging in with tmp_pass, so change it to be their real pass
   $u->of(false);
   $u->pass = $u->tmp_pass;
   $u->save();
   $u->of(true);
 }
 $u = $session->login($username, $pass); 
 if($u) {
   // user is logged in, get rid of tmp_pass
   $u->of(false);
   $u->tmp_pass = '';
   $u->save();
   // now redirect to the profile edit page
   $session->redirect('/profile/'); 
 }
}

// present the login form
$headline = $input->post->username ? "Login failed" : "Please login";
$page->body = "
 <h2>$headline</h2>
 <form action='./' method='post'>
 <p>
 <label>Username <input type='text' name='username'></label>
 <label>Password <input type='password' name='pass'></label>
 </p>
 <input type='submit'>
 </form>
 <p><a href='/reset-pass/'>Forgot your password?</a></p>
";

include("./main.php"); // main markup template

3b. Create a /login/ page that uses the above template.

4a. Build a profile editing template that at least lets them change their password (but take it further if you want):

/site/templates/profile.php

// if user isn't logged in, then we pretend this page doesn't exist
if(!$user->isLoggedin()) throw new Wire404Exception(); 

// check if they submitted a password change
$pass = $input->post->pass; 
if($pass) {
 if(strlen($pass) < 6) {
   $page->body .= "<p>New password must be 6+ characters</p>";
 } else if($pass !== $input->post->pass_confirm) {
   $page->body .= "<p>Passwords do not match</p>";
 } else {
   $user->of(false);
   $user->pass = $pass; 
   $user->save();
   $user->of(true);
   $page->body .= "<p>Your password has been changed.</p>";
 }
}
// display a password change form
$page->body .= "
 <h2>Change password</h2>
 <form action='./' method='post'>
 <p>
 <label>New Password <input type='password' name='pass'></label><br>
 <label>New Password (confirm) <input type='password' name='pass_confirm'></label>
 </p>
 <input type='submit'>
 </form>
 <p><a href='/logout/'>Logout</a></p>
";
include("./main.php"); 

4b. Create a page called /profile/ that uses the template above.

5. Just to be complete, make a logout.php template and create a page called /logout/ that uses it.

/site/templates/logout.php

if($user->isLoggedin()) $session->logout();
$session->redirect('/'); 

6. The above templates include main.php at the end. This should just be an HTML document that outputs your site's markup, like a separate head.inc or foot.inc would do, except that it's all in one file and called after the output is generated, and we leave the job of sending the output to main.php. An example of the simplest possible main.php would be:

/site/templates/main.php

<html>
<head>
 <title><?=$page->title?></title>
</head>
<body>
 <?=$page->body?>
</body>
</html>
  • Like 49

Share this post


Link to post
Share on other sites

...and among a million other reasons, it's responses like this that make Processwire incredible.

There's a lot in there I can learn from. Thanks for sharing.

:)

  • Like 6

Share this post


Link to post
Share on other sites

Hi Ryan,

Is there any downside to using something like this to create a random/temporary password?

$pass = uniqid(); // generate random pass
$user->pass = $pass; // Update PW with random pass

It seems to work fine for me, I'm just curious if there's a reason not to use this method.

Share this post


Link to post
Share on other sites

The disadvantage with uniqid() is that its output is predictable since the value returned is based on the current timestamp. So on the surface, it seems there's possibility for that to be exploited, although it might be challenging.

The password generation I put in the example above is based all around random characters, which aren't predictable other than the set which they come from (a-z2-9) and the length (between 9 and 12). I think this is pretty strong. However, the security of it could be increased by adding more to the set of possible characters (punctuation, upper/lower, etc.) and increasing the possible length range, like up to 30 characters or something. I opted to limit the character set and exclude the letters that are easily confused with each other, like "o O 0" and "i I l 1", just in case people are hand-typing (rather than copy/pasting)… less support burden.

Another thing I might add to the code would be to make it only work with users of a certain role. But that would be pretty site-specific, so left it out here.

  • Like 5

Share this post


Link to post
Share on other sites

That makes sense, and I like that your method allows you to omit commonly confused characters, and specify the length of the password.

Like I said, lots in there for me to learn from. ;)

  • Like 1

Share this post


Link to post
Share on other sites

Thank you Ryan & renobird. I love the big explanations & the time you took to write this all down.

...and among a million other reasons, it's responses like this that make Processwire incredible.

There's a lot in there I can learn from. Thanks for sharing.

:)

Total Right !

continue with the fight now....

  • Like 1

Share this post


Link to post
Share on other sites

ryan said: 1. In Admin > Setup > Fields, create a new text field called 'tmp_pass' and add it to the 'user' template.

I wished to do this from within the "public function ___install()" in the module.

But I can't find anything on creating new fields, can someone help me out?

dumb me:

Edited by Martijn Geerts

Share this post


Link to post
Share on other sites

I wished to do this from within the "public function ___install()" in the module.

But I can't find anything on creating new fields, can someone help me out?

Have a look here: http://processwire.c...hout-using-gui/

Or on modules like shop by apeisa there's lots examples.

  • Like 1

Share this post


Link to post
Share on other sites

tnx for your advise ! Will take a look !

Slowly learning & I hope I can manage to complete the module. ( first OOP project for me )

Share this post


Link to post
Share on other sites
<?php if sense_of_humor ?>

Wow, and we have to do everything ourselves?

<?php endif ?>

Great topic, really learned a lot. :)

EDIT:

Everything working except that part where reset email was sent, password did not reset at all. Would be also nice to have another page which really resets the password, ie email will include link to that page. That way, if user ignores the email, no real changes would be made.

Share this post


Link to post
Share on other sites
Everything working except that part where reset email was sent, password did not reset at all. Would be also nice to have another page which really resets the password, ie email will include link to that page. That way, if user ignores the email, no real changes would be made.

If you are going off one of my earlier posts in here, this was the reason for the new tmp_pass variable. No changes are made to the password unless the user is logged in and changes the password manually. The tmp_pass is a one-time code that expires as soon as you login (whether using the real pass or the tmp_pass). The tmp_pass code is only visible at the account owner's email. I've found this method works well for simple account authentication situations. Of course, it does assume the user's email is secure, as many "password reset" functions do. For high security situations, you don't want to trust that the user's email is secure.

  • Like 1

Share this post


Link to post
Share on other sites

Thanks, that explains a lot. Just perfect for my needs. Checked my log and found out that my error is actually coming from not being able to create user template.(?)

EDIT: the actual error is "TemplateFile : Duplicate entry 'user' for key 'name' INSERT INTO `fieldgroups` SET `name`='user'"

When you say: add it to the 'user' template, is 'user' built-in template or something else?

Share this post


Link to post
Share on other sites

It is built in template, you find it from templates, filter, show built in templates.

  • Like 1

Share this post


Link to post
Share on other sites

I have copied Ryan's custom login code and tested out on my server and it work great! Thank you so much Ryan!

Since I was testing with various users which I just created using Admin panel and provided same email address to different users and I find that I still receive email with temp password but when I supply username with new temp password on the custom login screen I got "Login Failed!" message! I was wondering if its going to work with same email address for different users?

Share this post


Link to post
Share on other sites

Code works perfectly, but I guess that changes in 'user' template (as well as in any other built-in templates) is not good as changes have to be applied again after each PW upgrade.

Correct? If yes, what is the best way to avoid this?

Perhaps workaround could be to keep usernames/passwords/emails in admin template and create custom 'site-user' template which can keep any other fields ('tmp-pass' in this case). This also requires unique key to match PW user and 'site-user' in the code, and can be done with help of username or email field (must be added to 'site-user' template).

Please advice if logic regarding usage of admin templates is correct and workaround is optimal. Thanks! 

Share this post


Link to post
Share on other sites

That's not correct. Nothing you do will get overwritten on update you just replace /wire/ folder, unless you modify something in there you're save to change whatever template you wish and it will stay there.

  • Like 1

Share this post


Link to post
Share on other sites

Ok, clear now. Thanks a lot!

Share this post


Link to post
Share on other sites

Been using Ryans code for the login page and I'm finding I get a server error if the user enters a username which includes an @

the error seems to be coming from:

$username = $sanitizer->username($input->post->username);
$pass = $input->post->pass; // this line has nothing to do with the problem, just left it in for consistency
$u = $users->get($username);

When sanitizing the username, the value we get back is not loved by the $users selector.

The error is logged as:

2013-04-13 16:27:19    guest    http://127.0.0.1/login/    Error:     Exception: Unknown Selector operator: '' -- was your selector value properly escaped? (in C:\xampp\htdocs\processwire\wire\core\Selectors.php line 165)

For myself I didn't need the user to be able to enter @ as I'm not passing an email, therefore I've flipped out the sanitizer to use name instead, i.e.

$username = $sanitizer->name($input->post->username);

I assumed this was a bug with the username sanitizer, however if there's a code change I could make it would be great to hear.

  • Like 1

Share this post


Link to post
Share on other sites

$sanitizer->username() is a deprecated holdout from PW 2.0, which didn't store users as pages and email addresses were allowed as usernames. So that's a typo on my part as the username() call should have been $sanitizer->pageName(), as users are themselves pages. 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...