fix password reset link.
[sgn.git] / lib / SGN / Controller / AJAX / User.pm
blobe38baaaa036f8383f3c92a78cee1570bb466f28d
2 package SGN::Controller::AJAX::User;
4 use Moose;
5 use IO::File;
6 use Data::Dumper;
8 BEGIN { extends 'Catalyst::Controller::REST' };
10 __PACKAGE__->config(
11 default => 'application/json',
12 stash_key => 'rest',
13 map => { 'application/json' => 'JSON', 'text/html' => 'JSON' },
17 sub login : Path('/ajax/user/login') Args(0) {
18 my $self = shift;
19 my $c = shift;
21 my $username = $c->req->param("username");
22 my $password = $c->req->param("password");
23 my $goto_url = $c->req->param("goto_url");
25 print STDERR "Goto URL = $goto_url\n";
27 my $login = CXGN::Login->new($c->dbc->dbh());
28 my $login_info = $login->login_user($username, $password);
30 if (exists($login_info->{incorrect_password}) && $login_info->{incorrect_password} == 1) {
31 $c->stash->{rest} = { error => "Login credentials are incorrect. Please try again." };
32 return;
34 elsif (exists($login_info->{account_disabled}) && $login_info->{account_disabled}) {
35 $c->stash->{rest} = { error => "This account has been disabled due to $login_info->{account_disabled}. Please contact the database to fix this problem." };
36 return;
38 else {
39 $c->stash->{rest} = {
40 message => "Login successful",
41 goto_url => $goto_url
42 };
46 sub logout :Path('/ajax/user/logout') Args(0) {
47 my $self = shift;
48 my $c = shift;
50 my $login = CXGN::Login->new($c->dbc->dbh());
51 $login->logout_user();
53 $c->stash->{rest} = { message => "User successfully logged out." };
56 sub new_account :Path('/ajax/user/new') Args(0) {
57 my $self = shift;
58 my $c = shift;
60 print STDERR "Adding new account...\n";
61 if ($c->config->{is_mirror}) {
62 $c->stash->{template} = '/system_message.mas';
63 $c->stash->{message} = "This site is a mirror site and does not support adding users. Please go to the main site to create an account.";
64 return;
68 my ($first_name, $last_name, $username, $password, $confirm_password, $email_address, $organization)
69 = map { $c->req->params->{$_} } (qw|first_name last_name username password confirm_password email_address organization|);
71 if ($username) {
73 # check password properties...
75 my @fail = ();
76 if (length($username) < 7) {
77 push @fail, "Username is too short. Username must be 7 or more characters";
78 } else {
79 # does user already exist?
81 my $existing_login = CXGN::People::Login -> get_login($c->dbc()->dbh(), $username);
83 if ($existing_login->get_username()) {
84 push @fail, "Username \"$username\" is already in use. Please pick a different username.";
88 if (length($password) < 7) {
89 push @fail, "Password is too short. Password must be 7 or more characters";
91 if ("$password" ne "$confirm_password") {
92 push @fail, "Password and confirm password do not match.";
95 if (!$organization) {
96 push @fail, "'Organization' is required.'";
99 if ($password eq $username) {
100 push @fail, "Password must not be the same as your username.";
102 if ($email_address !~ m/[^\@]+\@[^\@]+/) {
103 push @fail, "Email address is invalid.";
105 unless($first_name) {
106 push @fail,"You must enter a first name or initial.";
108 unless($last_name) {
109 push @fail,"You must enter a last name.";
112 if (@fail) {
113 $c->stash->{rest} = { error => "Account creation failed for the following reason(s): ".(join ", ", @fail) };
114 return;
118 my $confirm_code = $self->tempname();
119 my $new_user = CXGN::People::Login->new($c->dbc->dbh());
120 $new_user -> set_username($username);
121 $new_user -> set_pending_email($email_address);
122 $new_user -> set_disabled('unconfirmed account');
123 $new_user -> set_organization($organization);
124 $new_user -> store();
126 print STDERR "Generated sp_person_id ".$new_user->get_sp_person_id()."\n";
127 print STDERR "Update password and confirm code...\n";
128 $new_user->update_password($password);
129 $new_user->update_confirm_code($confirm_code);
131 print STDERR "Store Person object...\n";
132 #this is being added because the person object still uses two different objects, despite the fact that we've merged the tables
133 my $person_id=$new_user->get_sp_person_id();
134 my $new_person=CXGN::People::Person->new($c->dbc->dbh(),$person_id);
135 $new_person->set_first_name($first_name);
136 $new_person->set_last_name($last_name);
137 $new_person->store();
139 my $host = $c->config->{main_production_site_url};
140 my $subject="[SGN] Email Address Confirmation Request";
141 my $body=<<END_HEREDOC;
143 Please do *NOT* reply to this message. The return address is not valid.
144 Use the <a href="/contact/form">contact form</a> instead.
146 This message is sent to confirm the email address for community user
147 \"$username\"
149 Please click (or cut and paste into your browser) the following link to
150 confirm your account and email address:
152 $host/solpeople/account-confirm.pl?username=$username&confirm=$confirm_code
154 Thank you,
155 Sol Genomics Network
157 END_HEREDOC
159 CXGN::Contact::send_email($subject,$body,$email_address);
160 $c->stash->{rest} = { message => qq | <table summary="" width="80%" align="center">
161 <tr><td><p>Account was created with username \"$username\". To continue, you must confirm that SGN staff can reach you via email address \"$email_address\". An email has been sent with a URL to confirm this address. Please check your email for this message and use the link to confirm your email address.</p></td></tr>
162 <tr><td><br /></td></tr>
163 </table>
164 | };
168 sub change_account_info_action :Path('/ajax/user/update') Args(0) {
169 my $self = shift;
170 my $c = shift;
172 if (! $c->user() ) {
173 $c->stash->{rest} = { error => "You must be logged in to use this page." };
174 return;
177 my $person = new CXGN::People::Login($c->dbc->dbh(), $c->user->get_sp_person_id());
179 # my ($current_password, $change_username, $change_password, $change_email) = $c->req->param({qw(current_password change_username change_password change_email)});
181 my $args = $c->req->params();
183 if (!$args->{change_password} && ! $args->{change_username} && !$args->{change_email}) {
184 my $error = "No actions were requested. Please select which fields you would like to update by checking the appropriate checkbox(es) on the form and entering your new information.";
185 print STDERR $error;
186 $c->stash->{rest} = { error => $error };
187 return;
190 chomp($args->{current_password});
191 if (! $person->verify_password($args->{current_password})) {
192 my $error = "Your current password does not match SGN records.";
193 print STDERR $error;
194 $c->stash->{rest} = { error => "$error" };
195 return;
198 # Check for error conditions in all changes, before making any of them.
199 # Otherwise, we could end up making some changes and then failing on later
200 # ones. The user would then push the back button and their information may
201 # be different now but they will probably assume no changes were made. This
202 # is most troublesome if the current password changes.
204 if ($args->{change_username}) {
205 #unless change_username is set, new_username won't be in the args hash because of the prestore test
206 my $new_username = $args->{new_username};
207 if(length($new_username) < 7) {
208 my $error = "Username must be at least 7 characters long.";
209 print STDERR $error;
210 $c->stash->{rest} = { error => $error };
211 return;
214 my $other_user = CXGN::People::Login->get_login($c->dbc->dbh(), $new_username);
215 if (defined $other_user->get_sp_person_id() &&
216 ($person -> get_sp_person_id() != $other_user->get_sp_person_id())) {
217 print STDERR "Username alread in use.\n";
218 $c->stash->{rest} = { error => "Username \"$new_username\" is already in use. Please select a different username." };
219 return;
222 $person->set_username($new_username);
223 $person->store();
226 if ($args->{change_password}) {
227 #unless change_password is set, new_password won't be in the args hash because of the prestore test
228 my ($new_password, $confirm_password) = ($args->{new_password}, $args->{confirm_password});
229 if(length($args->{new_password}) < 7) {
230 print STDERR "Password too short\n";
231 $c->stash->{rest} = { error => "Passwords must be at least 7 characters long. Please try again." };
232 return;
234 #format check
235 if($args->{new_password} !~ /^[a-zA-Z0-9~!@#$^&*_.=:;<>?]+$/) {
236 print STDERR "Illegal characters in password\n";
237 $c->stash->{rest} = { error => "An error occurred. Please use your browser's back button to try again.. The Password can't contain spaces or these symbols: <u><b>` ( ) [ ] { } - + ' \" / \\ , |</b></u>." };
238 return;
240 if($args->{new_password} ne $args->{confirm_password}) {
241 print STDERR "Password don't match.\n";
242 $c->stash->{rest} = { error => "New password entries do not match. You must enter your new password twice to verify accuracy." };
243 return;
246 print STDERR "Saving new password to the database\n";
247 $person->update_password($args->{new_password});
250 my $user_private_email = $c->user->get_private_email();
251 if($args->{change_email}) {
252 #unless change_email is set, private_email won't be in the args hash because of the prestore test
253 my ($private_email, $confirm_email) = ($args->{private_email}, $args->{confirm_email});
254 if($private_email !~ m/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+$/) {
255 print STDERR "Invalid email address\n";
256 $c->stash->{rest} = { error => "An error occurred. Please use your browser's back button to try again. The E-mail address \"$private_email\" does not appear to be a valid e-mail address." };
257 return;
259 if($private_email ne $confirm_email) {
260 print STDERR "Emails don't match\n";
261 $c->stash->{rest} = { error => "An error occurred. Please use your browser's back button to try again. New e-mail address entries do not match. You must enter your new e-mail address twice to verify accuracy." };
262 return;
265 print STDERR "Saving private email '$private_email' to the database\n";
266 $person->set_private_email($private_email);
267 my $confirm_code = $self->tempname();
268 $person->set_confirm_code($confirm_code);
269 $person->store();
271 $user_private_email = $private_email;
273 $self->send_confirmation_email($args->{username}, $user_private_email, $confirm_code, $c->config->{main_production_site_url});
277 $c->stash->{rest} = { message => "Update successful" };
281 sub send_confirmation_email {
282 my ($self, $username, $private_email, $confirm_code, $host) = @_;
283 my $subject = "[SGN] E-mail Address Confirmation Request";
285 my $body = <<END_HEREDOC;
287 You requested an account on the site $host.
289 Please do *NOT* reply to this message. The return address is not valid.
290 Use the contact form at $host/contact/form instead.
292 This message is sent to confirm the private e-mail address for community user
293 \"$username\".
295 Please click (or cut and paste into your browser) the following link to
296 confirm your account and e-mail address:
298 $host/user/confirm?username=$username&confirm=$confirm_code
300 Thank you.
301 Sol Genomics Network
302 END_HEREDOC
304 CXGN::Contact::send_email($subject, $body, $private_email);
307 sub reset_password :Path('/ajax/user/reset_password') Args(0) {
308 my $self = shift;
309 my $c = shift;
311 my $email = $c->req->param('password_reset_email');
313 my @person_ids = CXGN::People::Login->get_login_by_email($c->dbc->dbh(), $email);
315 if (!@person_ids) {
316 $c->stash->{rest} = { error => "The provided email ($email) is not associated with any account." };
317 return;
320 if (@person_ids > 1) {
321 $c->stash->{rest} = { message => "The provided email ($email) is associated with multiple accounts. An email is sent for each account. Please notify the database team using the contact form to consolidate the accounts." };
324 my $reset_link = "";
325 foreach my $pid (@person_ids) {
326 my $email_reset_token = $self->tempname();
327 $reset_link = $c->config->{main_production_site_url}."/user/reset_password_form?reset_password_token=$email_reset_token";
328 my $person = CXGN::People::Login->new( $c->dbc->dbh(), $pid);
329 $person->update_confirm_code($email_reset_token);
330 print STDERR "Sending reset link $reset_link\n";
331 $self->send_reset_email_message($c, $pid, $email, $reset_link);
334 $c->stash->{rest} = { message => "Reset link sent. Please check your email and click on the link." };
337 sub process_reset_password_form :Path('/ajax/user/process_reset_password') Args(0) {
338 my $self = shift;
339 my $c = shift;
341 my $token = $c->req->param("token");
342 my $new_password = $c->req->param("");
344 eval {
345 my $sp_person_id = CXGN::People::Login->get_login_by_token($c->dbc->dbh, $token);
347 my $login = CXGN::People::Login->new($c->dbc->dbh(), $sp_person_id);
348 $login->update_password($new_password);
349 $login->update_confirm_code("");
351 if ($@) {
352 $c->stash->{rest} = { error => $@ };
354 else {
355 $c->stash->{rest} = { message => "The password was successfully updated." };
361 sub send_reset_email_message {
362 my $self = shift;
363 my $c = shift;
364 my $pid = shift;
365 my $private_email = shift;
366 my $reset_link = shift;
368 my $subject = "[SGN] E-mail Address Confirmation Request";
369 my $main_url = $c->config->{main_production_site_url};
371 my $body = <<END_HEREDOC;
375 you have requested a password reset on $main_url.
377 If this request did not come from you, please let us know.
379 To contact us, please do NOT reply to this message; rather, use the contact form ($main_url/contact/form) instead.
381 Your password can be reset using the following link, which you can either click or cut and paste into your browser:
383 $reset_link
385 Thank you.
387 Your friends at $main_url
389 END_HEREDOC
391 CXGN::Contact::send_email($subject, $body, $private_email);
394 sub tempname {
395 my $self = shift;
396 my $rand_string = "";
397 my $dev_urandom = new IO::File "</dev/urandom" || print STDERR "Can't open /dev/urandom";
398 $dev_urandom->read( $rand_string, 16 );
399 my @bytes = unpack( "C16", $rand_string );
400 $rand_string = "";
401 foreach (@bytes) {
402 $_ %= 62;
403 if ( $_ < 26 ) {
404 $rand_string .= chr( 65 + $_ );
406 elsif ( $_ < 52 ) {
407 $rand_string .= chr( 97 + ( $_ - 26 ) );
409 else {
410 $rand_string .= chr( 48 + ( $_ - 52 ) );
413 return $rand_string;
416 sub get_login_button_html :Path('/ajax/user/login_button_html') Args(0) {
417 my $self = shift;
418 my $c = shift;
419 eval {
420 my $production_site = $c->config->{main_production_site_url};
421 my $html = "";
422 # if the site is a mirror, gray out the login/logout links
423 if( $c->config->{'is_mirror'} ) {
424 print STDERR "generating login button for mirror site...\n";
425 $html = <<HTML;
426 <a style="line-height: 1.2; text-decoration: underline; background: none" href="$production_site" title="log in on main site">main site</a>
427 } elsif ( $c->config->{disable_login} ) {
428 <li class="dropdown">
429 <div class="btn-group" role="group" aria-label="..." style="height:34px; margin: 1px 0px 0px 0px" >
430 <button class="btn btn-primary disabled" type="button" style="margin: 7px 7px 0px 0px">Login</button>
431 </div>
432 </li>
434 HTML
436 } elsif ( $c->req->uri->path_query =~ "logout=yes") {
437 print STDERR "generating login button for logout...\n";
438 $html = <<HTML;
439 <li class="dropdown">
440 <div class="btn-group" role="group" aria-label="..." style="height:34px; margin: 1px 0px 0px 0px" >
441 <a href="/user/login">
442 <button class="btn btn-primary" type="button" style="margin: 7px 7px 0px 0px">Login</button>
443 </a>
444 </div>
445 </li>
446 HTML
448 } elsif ( $c->user_exists ) {
449 print STDERR "Generate login button for logged in user...\n";
450 my $sp_person_id = $c->user->get_object->get_sp_person_id;
451 my $username = $c->user->get_username();
452 $html = <<HTML;
453 <li>
454 <div class="btn-group" role="group" aria-label="..." style="height:34px; margin: 1px 3px 0px 0px">
455 <button id="navbar_profile" class="btn btn-primary" type="button" onclick='location.href="/solpeople/profile/$sp_person_id"' style="margin: 7px 0px 0px 0px" title="My Profile">$username</button>
456 <button id="navbar_lists" name="lists_link" class="btn btn-info" style="margin:7px 0px 0px 0px" type="button" title="Lists" onClick="show_lists();">
457 Lists <span class="glyphicon glyphicon-list-alt" ></span>
458 </button>
459 <button id="navbar_personal_calendar" name="personal_calendar_link" class="btn btn-primary" style="margin:7px 0px 0px 0px" type="button" title="Your Calendar">Calendar&nbsp;<span class="glyphicon glyphicon-calendar" ></span>
460 </button>
461 <button id="navbar_logout" class="btn btn-default glyphicon glyphicon-log-out" style="margin:6px 0px 0px 0px" type="button" onclick="logout();" title="Logout"></button>
462 </div>
463 </li>
464 HTML
466 } else {
467 print STDERR "generating regular login button..\n";
468 $html = qq |
469 <li class="dropdown">
470 <div class="btn-group" role="group" aria-label="..." style="height:34px; margin: 1px 0px 0px 0px" >
471 <button id="site_login_button" name="site_login_button" class="btn btn-primary" type="button" style="margin: 7px 7px 0px 0px; position-absolute: 10,10,100,10">Login</button>
472 </div>
473 </li>
477 if ($@) {
478 print STDERR "ERROR: $@\n";
479 $c->stash->{rest} = { error => $@ };
481 return $c->stash->{rest} = { html => $html };