[freeside-commits] branch master updated. cb6cca67db487271ce96b49289ada58691a2067d

Ivan ivan at 420.am
Mon Dec 30 01:43:21 PST 2013


The branch, master has been updated
       via  cb6cca67db487271ce96b49289ada58691a2067d (commit)
      from  6b4a2501a75964c864467a3bf85bbba039009049 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit cb6cca67db487271ce96b49289ada58691a2067d
Author: Ivan Kohler <ivan at freeside.biz>
Date:   Mon Dec 30 01:43:16 2013 -0800

    self-service access for contacts, RT#25533

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index aa21ac0..37d21ea 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -45,6 +45,7 @@ use FS::cust_pkg;
 use FS::payby;
 use FS::acct_rt_transaction;
 use FS::msg_template;
+use FS::contact;
 
 $DEBUG = 1;
 $me = '[FS::ClientAPI::MyAccount]';
@@ -210,6 +211,7 @@ sub login {
   my $conf = new FS::Conf;
 
   my $svc_x = '';
+  my $session = {};
   if ( $p->{'domain'} eq 'svc_phone'
        && $conf->exists('selfservice_server-phone_login') ) { 
 
@@ -227,8 +229,19 @@ sub login {
 
     $svc_x = $svc_phone;
 
+  } elsif ( $p->{email}
+              && (my $contact = FS::contact->by_selfservice_email($p->{email}))
+          )
+  {
+    return { error => 'Incorrect password.' }
+      unless $contact->authenticate_password($p->{'password'});
+
+    $session->{'custnum'} = $contact->custnum;
+
   } else {
 
+    ( $p->{username}, $p->{domain} ) = split('@', $p->{email}) if $p->{email};
+
     my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
       or return { error => 'Domain '. $p->{'domain'}. ' not found' };
 
@@ -251,31 +264,33 @@ sub login {
 
   }
 
-  my $session = {
-    'svcnum' => $svc_x->svcnum,
-  };
+  if ( $svc_x ) {
+
+    $session->{'svcnum'} = $svc_x->svcnum;
 
-  my $cust_svc = $svc_x->cust_svc;
-  my $cust_pkg = $cust_svc->cust_pkg;
-  if ( $cust_pkg ) {
-    my $cust_main = $cust_pkg->cust_main;
-    $session->{'custnum'} = $cust_main->custnum;
-    if ( $conf->exists('pkg-balances') ) {
-      my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
-                          $cust_main->ncancelled_pkgs;
-      $session->{'pkgnum'} = $cust_pkg->pkgnum
-        if scalar(@cust_pkg) > 1;
+    my $cust_svc = $svc_x->cust_svc;
+    my $cust_pkg = $cust_svc->cust_pkg;
+    if ( $cust_pkg ) {
+      my $cust_main = $cust_pkg->cust_main;
+      $session->{'custnum'} = $cust_main->custnum;
+      if ( $conf->exists('pkg-balances') ) {
+        my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
+                            $cust_main->ncancelled_pkgs;
+        $session->{'pkgnum'} = $cust_pkg->pkgnum
+          if scalar(@cust_pkg) > 1;
+      }
     }
-  }
 
-  #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
-  #return { error => 'Only primary user may log in.' } 
-  #  if $conf->exists('selfservice_server-primary_only')
-  #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
-  my $part_pkg = $cust_pkg->part_pkg;
-  return { error => 'Only primary user may log in.' }
-    if $conf->exists('selfservice_server-primary_only')
-       && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+    #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
+    #return { error => 'Only primary user may log in.' } 
+    #  if $conf->exists('selfservice_server-primary_only')
+    #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
+    my $part_pkg = $cust_pkg->part_pkg;
+    return { error => 'Only primary user may log in.' }
+      if $conf->exists('selfservice_server-primary_only')
+         && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+
+  }
 
   my $session_id;
   do {
@@ -2834,6 +2849,54 @@ sub myaccount_passwd {
 
 }
 
+#regular pw change in self-service should change contact pw too, otherwise its way too confusing.  hell its confusing they're separate at all, but alas.  need to support the "ISP provides email that's used as a contact email" case as well as we can.
+#  sub contact_passwd {
+#    my $p = shift;
+#    my($context, $session, $custnum) = _custoragent_session_custnum($p);
+#    return { 'error' => $session } if $context eq 'error';
+#  
+#    return { 'error' => 'Not logged in as a contact.' }
+#      unless $session->{'contactnum'};
+#  
+#    return { 'error' => "New passwords don't match." }
+#      if $p->{'new_password'} ne $p->{'new_password2'};
+#  
+#    return { 'error' => 'Enter new password' }
+#      unless length($p->{'new_password'});
+#  
+#    #my $search = { 'custnum' => $custnum };
+#    #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+#    $custnum =~ /^(\d+)$/ or die "illegal custnum";
+#    my $search = " AND selfservice_access IS NOT NULL ".
+#                 " AND selfservice_access = 'Y' ".
+#                 " AND ( disabled IS NULL OR disabled = '' )".
+#                 " AND custnum IS NOT NULL AND custnum = $1";
+#    $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
+#  
+#    my $contact = qsearchs( {
+#      'table'     => 'contact',
+#      'addl_from' => 'LEFT JOIN cust_main USING ( custnum ) ',
+#      'hashref'   => { 'contactnum' => $session->{'contactnum'}, },
+#      'extra_sql' => $search, #important
+#    } )
+#      or return { 'error' => "Email not found" }; #?  how did we get logged in?
+#                                                  # deleted since then?
+#  
+#    my $error = '';
+#  
+#    # use these svc_acct length restrictions??
+#    my $conf = new FS::Conf;
+#    $error = 'Password too short.'
+#      if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6);
+#    $error = 'Password too long.'
+#      if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8);
+#  
+#    $error ||= $contact->change_password($p->{'new_password'});
+#  
+#    return { 'error' => $error, };
+#  
+#  }
+
 sub reset_passwd {
   my $p = shift;
 
@@ -2841,22 +2904,57 @@ sub reset_passwd {
   my $verification = $conf->config('selfservice-password_reset_verification')
     or return { 'error' => 'Password resets disabled' };
 
-  my $username = $p->{'username'};
+  my $contact = '';
+  my $svc_acct = '';
+  my $cust_main = '';
+  if ( $p->{'email'} ) { #new-style, changes contact and svc_acct
+  
+    $contact = FS::contact->by_selfservice_email($p->{'email'});
 
-  my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
-    or return { 'error' => 'Account not found' };
+    $cust_main = $contact->cust_main if $contact;
 
-  my $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
-                                        'domsvc'   => $svc_domain->svcnum  }
-                         )
-    or return { 'error' => 'Account not found' };
+    #also look for an svc_acct, otherwise it would be super confusing
 
-  my $cust_pkg = $svc_acct->cust_svc->cust_pkg
-    or return { 'error' => 'Account not found' };
+    my($username, $domain) = split('@', $p->{'email'});
+    my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } );
+    if ( $svc_domain ) {
+      $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
+                                         'domsvc'   => $svc_domain->svcnum  }
+                          );
+      if ( $svc_acct ) {
+        my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+        $cust_main ||= $cust_pkg->cust_main if $cust_pkg;
+
+        #precaution: don't change svc_acct password not part of the same
+        # customer as contact
+        $svc_acct = '' if ! $cust_pkg
+                       || $cust_pkg->custnum != $cust_main->custnum;
+      }
+      
+    }
+
+    return { 'error' => 'Email address not found' }
+      unless $contact || $svc_acct;
+
+  } elsif ( $p->{'username'} ) { #old style, looks in svc_acct only
+
+    my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
+      or return { 'error' => 'Account not found' };
+
+    $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
+                                       'domsvc'   => $svc_domain->svcnum  }
+                        )
+      or return { 'error' => 'Account not found' };
+
+    my $cust_pkg = $svc_acct->cust_svc->cust_pkg
+      or return { 'error' => 'Account not found' };
 
-  my $cust_main = $cust_pkg->cust_main;
+    $cust_main = $cust_pkg->cust_main;
+
+  }
 
   my %verify = (
+    'email'   => sub { 1; },
     'paymask' => sub { 
       my( $p, $cust_main ) = @_;
       $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/
@@ -2887,35 +2985,54 @@ sub reset_passwd {
 
   }
 
-  #okay, we're verified, now create a unique session
+  #okay, we're verified
 
-  my $reset_session = {
-    'svcnum' => $svc_acct->svcnum,
-  };
+  if ( $contact ) {
 
-  my $timeout = '1 hour'; #?
+    my $error = $contact->send_reset_email(
+                            'svcnum' => ($svc_acct ? $svc_acct->svcnum : ''),
+                          );
+
+    if ( $error ) {
+      return { 'error' => $error }; #????
+    }
+
+  } elsif ( $svc_acct ) {
+
+    #create a unique session
+
+    my $reset_session = {
+      'svcnum' => $svc_acct->svcnum,
+    };
+
+    my $timeout = '1 hour'; #?
+
+    my $reset_session_id;
+    do {
+      $reset_session_id = sha512_hex(time(). {}. rand(). $$)
+    } until ( ! defined _cache->get("reset_passwd_$reset_session_id") );
+      #just in case
+
+    _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
+
+    #email it
+
+    my $msgnum = $conf->config('selfservice-password_reset_msgnum',
+                               $cust_main->agentnum);
+    #die "selfservice-password_reset_msgnum unset" unless $msgnum;
+    return { 'error' => "selfservice-password_reset_msgnum unset" }
+      unless $msgnum;
+    my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
+    my $error = $msg_template->send( 'cust_main'     => $cust_main,
+                                     'object'        => $svc_acct,
+                                     'substitutions' => {
+                                       'session_id' => $reset_session_id,
+                                     }
+                                   );
+    if ( $error ) {
+      return { 'error' => $error }; #????
+    }
 
-  my $reset_session_id;
-  do {
-    $reset_session_id = sha512_hex(time(). {}. rand(). $$)
-  } until ( ! defined _cache->get("reset_passwd_$reset_session_id") ); #just in case
-
-  _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
-
-  #email it
-
-  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
-  #die "selfservice-password_reset_msgnum unset" unless $msgnum;
-  return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
-  my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
-  my $error = $msg_template->send( 'cust_main'     => $cust_main,
-                                   'object'        => $svc_acct,
-                                   'substitutions' => {
-                                     'session_id' => $reset_session_id,
-                                   }
-                                 );
-  if ( $error ) {
-    return { 'error' => $error }; #????
   }
 
   return { 'error' => '' };
@@ -2931,14 +3048,38 @@ sub check_reset_passwd {
   my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
     or return { 'error' => "Can't resume session" }; #better error message
 
-  my $svcnum = $reset_session->{'svcnum'};
+  if ( $reset_session->{'svcnum'} ) {
 
-  my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
-    or return { 'error' => "Service not found" };
+    my $svcnum = $reset_session->{'svcnum'};
 
-  return { 'error'    => '',
-           'username' => $svc_acct->username,
-         };
+    my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+      or return { 'error' => "Service not found" };
+
+    return { 'error'      => '',
+             'session_id' => $p->{'session_id'},
+             'username'   => $svc_acct->username,
+           };
+
+  } elsif ( $reset_session->{'contactnum'} ) {
+
+    my $contactnum = $reset_session->{'contactnum'};
+
+    my $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+      or return { 'error' => "Contact not found" };
+
+    my @contact_email = $contact->contact_email;
+    return { 'error' => 'No contact email' } unless @contact_email;
+
+    return { 'error'      => '',
+             'session_id' => $p->{'session_id'},
+             'email'      => $contact_email[0]->email, #the first?
+           };
+
+  } else {
+
+    return { 'error' => 'No svcnum or contactnum in session' }; #??
+
+  }
 
 }
 
@@ -2958,20 +3099,43 @@ sub process_reset_passwd {
   my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
     or return { 'error' => "Can't resume session" }; #better error message
 
-  my $svcnum = $reset_session->{'svcnum'};
+  if ( $reset_session->{'svcnum'} ) {
 
-  my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
-    or return { 'error' => "Service not found" };
+    my $svcnum = $reset_session->{'svcnum'};
 
-  $svc_acct->set_password($p->{'new_password'});
-  my $error = $svc_acct->replace();
+    my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+      or return { 'error' => "Service not found" };
 
-  my($label, $value) = $svc_acct->cust_svc->label;
+    $svc_acct->set_password($p->{'new_password'});
+    my $error = $svc_acct->replace();
 
-  return { 'error' => $error,
-           #'label' => $label,
-           #'value' => $value,
-         };
+    return { 'error' => $error } if $error;
+
+    #my($label, $value) = $svc_acct->cust_svc->label;
+    #return { 'error' => $error,
+    #         #'label' => $label,
+    #         #'value' => $value,
+    #       };
+
+  }
+
+  if ( $reset_session->{'contactnum'} ) {
+
+    my $contactnum = $reset_session->{'contactnum'};
+
+    my $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+      or return { 'error' => "Contact not found" };
+
+    my $error = $contact->change_password($p->{'new_password'});
+
+    return { 'error' => $error }; # if $error;
+
+  }
+
+  #password changed ,so remove session, don't want it reused
+  _cache->remove($p->{'session_id'});
+
+  return { 'error' => '' };
 
 }
 
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index eb47413..375d2d8 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2837,7 +2837,8 @@ and customer address. Include units.',
     'description' => 'If enabled, specifies the type of verification required for self-service password resets.',
     'type'        => 'select',
     'select_hash' => [ '' => 'Password reset disabled',
-                       'paymask,amount,zip' => 'Verify with credit card (or bank account) last 4 digits, payment amount and zip code',
+                       'email' => 'Click on a link in email',
+                       'paymask,amount,zip' => 'Click on a link in email, and also verify with credit card (or bank account) last 4 digits, payment amount and zip code',
                      ],
   },
 
diff --git a/FS/FS/Setup.pm b/FS/FS/Setup.pm
index e27b66f..29130fa 100644
--- a/FS/FS/Setup.pm
+++ b/FS/FS/Setup.pm
@@ -400,6 +400,9 @@ sub initial_data {
     #phone types
     'phone_type' => [],
 
+    #message templates
+    'msg_template' => [],
+
   ;
 
   \%hash;
diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm
index 3f73bd6..2a73b9c 100644
--- a/FS/FS/contact.pm
+++ b/FS/FS/contact.pm
@@ -2,8 +2,11 @@ package FS::contact;
 use base qw( FS::Record );
 
 use strict;
+use Scalar::Util qw( blessed );
 use FS::Record qw( qsearchs dbh ); # qw( qsearch qsearchs dbh );
 use FS::contact_phone;
+use FS::contact_email;
+use FS::queue;
 
 =head1 NAME
 
@@ -168,6 +171,14 @@ sub insert {
     }
   #}
 
+  if ( $self->selfservice_access ) {
+    my $error = $self->send_reset_email( queue=>1 );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -225,6 +236,12 @@ returns the error, otherwise returns false.
 sub replace {
   my $self = shift;
 
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $self->replace_old;
+
+  $self->$_( $self->$_ || $old->$_ ) for qw( _password _password_encoding );
+
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
   local $SIG{TERM} = 'IGNORE';
@@ -235,7 +252,7 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $error = $self->SUPER::replace(@_);
+  my $error = $self->SUPER::replace($old);
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -264,7 +281,7 @@ sub replace {
     }
   }
 
-  if ( defined($self->get('emailaddress')) ) {
+  if ( defined($self->hashref->{'emailaddress'}) ) {
 
     #ineffecient but whatever, how many email addresses can there be?
 
@@ -302,6 +319,19 @@ sub replace {
     }
   #}
 
+  if (    ( $old->selfservice_access eq '' && $self->selfservice_access
+              && ! $self->_password
+          )
+       || $self->_resend()
+     )
+  {
+    my $error = $self->send_reset_email( queue=>1 );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -383,6 +413,11 @@ and replace methods.
 sub check {
   my $self = shift;
 
+  if ( $self->selfservice_access eq 'R' ) {
+    $self->selfservice_access('Y');
+    $self->_resend('Y');
+  }
+
   my $error = 
     $self->ut_numbern('contactnum')
     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
@@ -425,6 +460,155 @@ sub contact_classname {
   $contact_class->classname;
 }
 
+sub by_selfservice_email {
+  my($class, $email) = @_;
+
+  my $contact_email = qsearchs({
+    'table'     => 'contact_email',
+    'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
+    'hashref'   => { 'emailaddress' => $email, },
+    'extra_sql' => " AND selfservice_access = 'Y' ".
+                   " AND ( disabled IS NULL OR disabled = '' )",
+  }) or return '';
+
+warn $contact_email;
+
+  $contact_email->contact;
+
+}
+
+#these three functions are very much false laziness w/FS/FS/Auth/internal.pm
+# and should maybe be libraried in some way for other password needs
+
+use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
+
+sub authenticate_password {
+  my($self, $check_password) = @_;
+
+  if ( $self->_password_encoding eq 'bcrypt' ) {
+
+    my( $cost, $salt, $hash ) = split(',', $self->_password);
+
+    my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
+                                               cost    => $cost,
+                                               salt    => de_base64($salt),
+                                             },
+                                             $check_password
+                                           )
+                              );
+
+    $hash eq $check_hash;
+
+  } else { 
+
+    return 0 if $self->_password eq '';
+
+    $self->_password eq $check_password;
+
+  }
+
+}
+
+sub change_password {
+  my($self, $new_password) = @_;
+
+  $self->change_password_fields( $new_password );
+
+  $self->replace;
+
+}
+
+sub change_password_fields {
+  my($self, $new_password) = @_;
+
+  $self->_password_encoding('bcrypt');
+
+  my $cost = 8;
+
+  my $salt = pack( 'C*', map int(rand(256)), 1..16 );
+
+  my $hash = bcrypt_hash( { key_nul => 1,
+                            cost    => $cost,
+                            salt    => $salt,
+                          },
+                          $new_password,
+                        );
+
+  $self->_password(
+    join(',', $cost, en_base64($salt), en_base64($hash) )
+  );
+
+}
+
+# end of false laziness w/FS/FS/Auth/internal.pm
+
+
+#false laziness w/ClientAPI/MyAccount/reset_passwd
+use Digest::SHA qw(sha512_hex);
+use FS::Conf;
+use FS::ClientAPI_SessionCache;
+sub send_reset_email {
+  my( $self, %opt ) = @_;
+
+  my @contact_email = $self->contact_email or return '';
+
+  my $reset_session = {
+    'contactnum' => $self->contactnum,
+    'svcnum'     => $opt{'svcnum'},
+  };
+
+  my $timeout = '24 hours'; #?
+
+  my $reset_session_id;
+  do {
+    $reset_session_id = sha512_hex(time(). {}. rand(). $$)
+  } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
+    #just in case
+
+  $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
+
+  #email it
+
+  my $conf = new FS::Conf;
+
+  my $cust_main = $self->cust_main
+    or die "no customer"; #reset a password for a prospect contact?  someday
+
+  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
+  #die "selfservice-password_reset_msgnum unset" unless $msgnum;
+  return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
+  my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
+  my %msg_template = (
+    'to'            => join(',', map $_->emailaddress, @contact_email ),
+    'cust_main'     => $cust_main,
+    'object'        => $self,
+    'substitutions' => { 'session_id' => $reset_session_id }
+  );
+
+  if ( $opt{'queue'} ) { #or should queueing just be the default?
+
+    my $queue = new FS::queue {
+      'job'     => 'FS::Misc::process_send_email',
+      'custnum' => $cust_main->custnum,
+    };
+    $queue->insert( $msg_template->prepare( %msg_template ) );
+
+  } else {
+
+    $msg_template->send( %msg_template );
+
+  }
+
+}
+
+use vars qw( $myaccount_cache );
+sub myaccount_cache {
+  #my $class = shift;
+  $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
+                         'namespace' => 'FS::ClientAPI::MyAccount',
+                       } );
+}
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index 5885921..2fc66b4 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -559,6 +559,9 @@ sub substitutions {
       [ company_phonenum  => sub {
           $conf->config('company_phonenum', shift->agentnum)
         } ],
+      [ selfservice_server_base_url => sub { 
+          $conf->config('selfservice_server-base_url') #, shift->agentnum) 
+        } ],
     ],
     # next_bill_date
     'cust_pkg'  => [qw( 
@@ -697,6 +700,10 @@ Returns the L<FS::agent> object for this template.
 sub _upgrade_data {
   my ($self, %opts) = @_;
 
+  ###
+  # First move any historical templates in config to real message templates
+  ###
+
   my @fixes = (
     [ 'alerter_msgnum',  'alerter_template',   '',               '', '' ],
     [ 'cancel_msgnum',   'cancelmessage',      'cancelsubject',  '', '' ],
@@ -788,6 +795,11 @@ sub _upgrade_data {
     } # if alerter_msgnum
 
   }
+
+  ###
+  # Move subject and body from msg_template to template_content
+  ###
+
   foreach my $msg_template ( qsearch('msg_template', {}) ) {
     if ( $msg_template->subject || $msg_template->body ) {
       # create new default content
@@ -811,6 +823,35 @@ sub _upgrade_data {
       die $error if $error;
     }
   }
+
+  ###
+  # Add new-style default templates if missing
+  ###
+  $self->_populate_initial_data;
+
+}
+
+sub _populate_initial_data { #class method
+  #my($class, %opts) = @_;
+  #my $class = shift;
+
+  eval "use FS::msg_template::InitialData;";
+  die $@ if $@;
+
+  my $initial_data = FS::msg_template::InitialData->_initial_data;
+
+  foreach my $hash ( @$initial_data ) {
+
+    next if $hash->{_conf} && $conf->config( $hash->{_conf} );
+
+    my $msg_template = new FS::msg_template($hash);
+    my $error = $msg_template->insert( @{ $hash->{_insert_args} || [] } );
+    die $error if $error;
+
+    $conf->set( $hash->{_conf}, $msg_template->msgnum ) if $hash->{_conf};
+  
+  }
+
 }
 
 sub eviscerate {
diff --git a/fs_selfservice/FS-SelfService/cgi/do_forgot_password.html b/fs_selfservice/FS-SelfService/cgi/do_forgot_password.html
new file mode 100644
index 0000000..adf8cca
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/do_forgot_password.html
@@ -0,0 +1,18 @@
+<HTML>
+  <HEAD>
+    <TITLE>Forgot password</TITLE>
+    <%= $head %>
+  </HEAD>
+  <BODY BGCOLOR="<%= $body_bgcolor || '#eeeeee' %>">
+  <%= $body_header %>
+
+<FONT SIZE=5>Forgot password</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+
+<%= if (!$error) {
+  $OUT .= 'A verification email has been sent to your mailbox.  Please follow
+           the link in your email to complete your password reset.';
+  }
+%>
+
+<%= $body_footer %>
diff --git a/fs_selfservice/FS-SelfService/cgi/do_process_forgot_password.html b/fs_selfservice/FS-SelfService/cgi/do_process_forgot_password.html
new file mode 100644
index 0000000..9274f92
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/do_process_forgot_password.html
@@ -0,0 +1,18 @@
+<HTML>
+  <HEAD>
+    <TITLE>Reset password</TITLE>
+    <%= $head %>
+  </HEAD>
+  <BODY BGCOLOR="<%= $body_bgcolor || '#eeeeee' %>">
+  <%= $body_header %>
+
+<FONT SIZE=5>Reset password</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+
+<%= if (!$error) {
+  $self_url =~ s/\?.*//;
+  $OUT .= "Your password has been changed.  You can now <A HREF=\"$self_url\">log in</A>.";
+  }
+%>
+
+<%= $body_footer %>
diff --git a/fs_selfservice/FS-SelfService/cgi/forgot_password.html b/fs_selfservice/FS-SelfService/cgi/forgot_password.html
new file mode 100644
index 0000000..e14034c
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/forgot_password.html
@@ -0,0 +1,53 @@
+<HTML>
+  <HEAD>
+    <TITLE>Forgot password</TITLE>
+    <%= $head %>
+  </HEAD>
+  <BODY BGCOLOR="<%= $body_bgcolor || '#eeeeee' %>">
+  <%= $body_header %>
+
+<FONT SIZE=5>Forgot password</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+
+<FORM ACTION="<%= $self_url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="action" VALUE="do_forgot_password">
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+
+Please enter your email address.  A password reset email will be sent to that address.
+
+<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+
+<TR>
+  <TH ALIGN="right">Email address </TH>
+  <TD>
+    <INPUT TYPE="text" NAME="email" VALUE="<%= $username %>"><%= $single_domain ? '@'.$single_domain : '' %>
+  </TD>
+</TR>
+
+<%=
+if ( $single_domain ) {
+
+  $OUT .= qq(<INPUT TYPE="hidden" NAME="domain" VALUE="$single_domain">);
+
+} else {
+
+  $OUT .= qq(
+    <TR>
+      <TH ALIGN="right">Domain </TH>
+      <TD>
+        <INPUT TYPE="text" NAME="domain" VALUE="$domain">
+      </TD>
+    </TR>
+  );
+
+}
+
+%>
+
+<TR>
+  <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" VALUE="Send reset email"></TD>
+</TR>
+</TABLE>
+</FORM>
+
+<%= $body_footer %>
diff --git a/fs_selfservice/FS-SelfService/cgi/login.html b/fs_selfservice/FS-SelfService/cgi/login.html
index 68f3ae4..65efd7b 100644
--- a/fs_selfservice/FS-SelfService/cgi/login.html
+++ b/fs_selfservice/FS-SelfService/cgi/login.html
@@ -15,25 +15,26 @@
 
 <TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=2 CELLPADDING=0>
 
-<TR>
-  <TH ALIGN="right">Username </TH>
-  <TD>
-    <INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"><%= $single_domain ? '@'.$single_domain : '' %>
-  </TD>
-</TR>
-
 <%=
 if ( $single_domain ) {
 
-  $OUT .= qq(<INPUT TYPE="hidden" NAME="domain" VALUE="$single_domain">);
+  $OUT .= qq(
+    <TR>
+      <TH ALIGN="right">Username </TH>
+      <TD>
+        <INPUT TYPE="text" NAME="username" VALUE="$username">\@$single_domain
+      </TD>
+    </TR>
+    <INPUT TYPE="hidden" NAME="domain" VALUE="$single_domain">
+  );
 
 } else {
 
   $OUT .= qq(
     <TR>
-      <TH ALIGN="right">Domain </TH>
+      <TH ALIGN="right">Email address </TH>
       <TD>
-        <INPUT TYPE="text" NAME="domain" VALUE="$domain">
+        <INPUT TYPE="text" NAME="email" VALUE="$email">
       </TD>
     </TR>
   );
diff --git a/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html b/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html
new file mode 100644
index 0000000..3d8c058
--- /dev/null
+++ b/fs_selfservice/FS-SelfService/cgi/process_forgot_password.html
@@ -0,0 +1,44 @@
+<HTML>
+  <HEAD>
+    <TITLE>Reset password</TITLE>
+    <%= $head %>
+  </HEAD>
+  <BODY BGCOLOR="<%= $body_bgcolor || '#eeeeee' %>">
+  <%= $body_header %>
+
+<FONT SIZE=5>Reset password</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+
+<FORM ACTION="<%= $self_url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="action" VALUE="do_process_forgot_password">
+<INPUT TYPE="hidden" NAME="session_id" VALUE="<%= $session_id %>">
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+
+<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+
+<%= if (!$error) {
+
+  $OUT .= <<'END';
+
+  <TR>
+    <TH ALIGN="right">New password: </TH>
+    <TD><INPUT TYPE="password" NAME="new_password" SIZE="18"></TD>
+  </TR>
+
+  <TR>
+    <TH ALIGN="right">Re-enter new password: </TH>
+    <TD><INPUT TYPE="password" NAME="new_password2" SIZE="18"></TD>
+  </TR>
+
+  <TR>
+    <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" VALUE="Change password"></TD>
+  </TR>
+END
+
+  }
+%>
+
+</TABLE>
+</FORM>
+
+<%= $body_footer %>
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
index ea2a40b..8d3a23b 100755
--- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
+++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
@@ -22,6 +22,7 @@ use FS::SelfService qw(
   adjust_ticket_priority
   mason_comp port_graph
   start_thirdparty finish_thirdparty
+  reset_passwd check_reset_passwd process_reset_passwd
 );
 
 $template_dir = '.';
@@ -42,54 +43,68 @@ if ( exists($cookies{'session'}) ) {
   if ( $session_id eq 'login' ) {
     # then we've just come back from the login page
 
-    $cgi->param('username') =~ /^\s*([a-z0-9_\-\.\&]{0,$form_max})\s*$/i;
-    my $username = $1;
-
-    $cgi->param('domain') =~ /^\s*([\w\-\.]{0,$form_max})\s*$/;
-    my $domain = $1;
-
     $cgi->param('password') =~ /^(.{0,$form_max})$/;
     my $password = $1;
 
-    if ( $username and $domain and $password ) {
+    if ( $cgi->param('email') =~ /^\s*([a-z0-9_\-\.\@]{1,$form_max})\s*$/i ) {
 
-      # authenticate
+      my $email = $1;
       $login_rv = login(
-        'username' => $username,
-        'domain'   => $domain,
-        'password' => $password,
+        'email'    => $email,
+        'password' => $password
       );
       $session_id = $login_rv->{'session_id'};
 
-    } elsif ( $username or $domain or $password ) {
+    } else {
+
+      $cgi->param('username') =~ /^\s*([a-z0-9_\-\.\&]{0,$form_max})\s*$/i;
+      my $username = $1;
+
+      $cgi->param('domain') =~ /^\s*([\w\-\.]{0,$form_max})\s*$/;
+      my $domain = $1;
+
+      if ( $username and $domain and $password ) {
+
+        # authenticate
+        $login_rv = login(
+          'username' => $username,
+          'domain'   => $domain,
+          'password' => $password,
+        );
+        $session_id = $login_rv->{'session_id'};
+
+      } elsif ( $username or $domain or $password ) {
       
-      my $error = 'Illegal '; #XXX localization...
-      my $count = 0;
-      if ( !$username ) {
-        $error .= 'username';
-        $count++;
-      }
-      if ( !$domain )  {
-        $error .= ', ' if $count;
-        $error .= 'domain';
-        $count++;
-      }
-      if ( !$password ) {
-        $error .= ', ' if $count;
-        $error .= 'and ' if $count > 1;
-        $error .= 'password';
-        $count++;
+        my $error = 'Illegal '; #XXX localization...
+        my $count = 0;
+        if ( !$username ) {
+          $error .= 'username';
+          $count++;
+        }
+        if ( !$domain )  {
+          $error .= ', ' if $count;
+          $error .= 'domain';
+          $count++;
+        }
+        if ( !$password ) {
+          $error .= ', ' if $count;
+          $error .= 'and ' if $count > 1;
+          $error .= 'password';
+          $count++;
+        }
+        $error .= '.';
+        $login_rv = {
+          'username'  => $username,
+          'domain'    => $domain,
+          'password'  => $password,
+          'error'     => $error,
+        };
+        $session_id = undef; # attempt login again
+
       }
-      $error .= '.';
-      $login_rv = {
-        'username'  => $username,
-        'domain'    => $domain,
-        'password'  => $password,
-        'error'     => $error,
-      };
-      $session_id = undef; # attempt login again
 
     } # else there was no input, so show no error message
+
   } # else session_id ne 'login'
 
 } else {
@@ -157,6 +172,10 @@ my @actions = ( qw(
   real_port_graph
   change_password
   process_change_password
+  forgot_password
+  do_forgot_password
+  process_forgot_password
+  do_process_forgot_password
   customer_suspend_pkg
   process_suspend_pkg
 ));
@@ -991,6 +1010,31 @@ sub process_change_password {
 
 }
 
+sub forgot_password {
+  login_info( 'agentnum' => scalar($cgi->param('agentnum')) ); #skin_info
+}
+
+sub do_forgot_password {
+  reset_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( email username domain )
+  );
+}
+
+sub process_forgot_password {
+  check_reset_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( session_id )
+  );
+}
+
+sub do_process_forgot_password {
+  process_reset_passwd(
+    map { $_ => scalar($cgi->param($_)) }
+      qw( session_id new_password new_password2 )
+  );
+}
+
 #--
 
 sub do_template {
diff --git a/httemplate/edit/cust_main-contacts.html b/httemplate/edit/cust_main-contacts.html
index cd83a29..9f06546 100644
--- a/httemplate/edit/cust_main-contacts.html
+++ b/httemplate/edit/cust_main-contacts.html
@@ -4,7 +4,7 @@
      'post_url'        => popurl(1). 'process/cust_main-contacts.html',
      'no_pkey_display' => 1,
      'labels'          => {
-                            'contactnum'  => 'Contact',
+                            'contactnum'  => ' ', #'Contact',
                             #'locationnum' => ' ',
                           },
      'fields'          => [
@@ -13,7 +13,7 @@
          'colspan'           => 6,
          'm2m_method'        => 'cust_contact',
          'm2m_dstcol'        => 'contactnum',   
-         'm2_label'          => 'Contact',
+         'm2_label'          => ' ', #'Contact',
          'm2_error_callback' => $m2_error_callback,
        },
      ],
diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html
index 06cac44..6c9d2f4 100644
--- a/httemplate/edit/msg_template.html
+++ b/httemplate/edit/msg_template.html
@@ -209,6 +209,7 @@ my %substitutions = (
     '$company_name'   => 'Our company name',
     '$company_address'=> 'Our company address',
     '$company_phonenum' => 'Our phone number',
+    '$selfservice_server_base_url' => 'Base URL of customer self-service',
   ],
   'contact' => [ # duplicate this for shipping
     '$name'           => 'Company and contact name',
diff --git a/httemplate/edit/process/cust_main-contacts.html b/httemplate/edit/process/cust_main-contacts.html
index ed874a5..d902dee 100644
--- a/httemplate/edit/process/cust_main-contacts.html
+++ b/httemplate/edit/process/cust_main-contacts.html
@@ -12,7 +12,9 @@
 %>
 <%init>
 
-my @contact_fields = qw( classnum first last title comment emailaddress );
+my @contact_fields = qw(
+  classnum first last title comment emailaddress selfservice_access
+);
 foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) {
   push @contact_fields, 'phonetypenum'.$phone_type->phonetypenum;
 }
diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html
index 8abce05..979c26b 100644
--- a/httemplate/elements/contact.html
+++ b/httemplate/elements/contact.html
@@ -45,14 +45,30 @@
 %       }
 
         <TD>
-          <INPUT TYPE  = "text"
-                 NAME  = "<%$name%>_<%$field%>"
-                 ID    = "<%$id%>_<%$field%>"
-                 SIZE  = "<% $size{$field} || 15 %>"
-                 VALUE = "<% scalar($cgi->param($name."_$field"))
-                             || $value |h %>"
-                 <% $onchange %>
-          ><BR>
+%         if ( $field eq 'selfservice_access' ) {
+            <SELECT NAME = "<%$name%>_<%$field%>"
+                    ID   = "<%$id%>_<%$field%>"
+            >
+              <OPTION VALUE="">Disabled
+%             if ( $value || $self_base_url ) {
+                <OPTION VALUE="Y" <% $value eq 'Y' ? 'SELECTED' : '' %>>Enabled
+%               if ( $value eq 'Y' && $self_base_url ) {
+                  <OPTION VALUE="R">Re-email
+%               }
+%             }
+            </SELECT>
+
+%         } else {
+            <INPUT TYPE  = "text"
+                   NAME  = "<%$name%>_<%$field%>"
+                   ID    = "<%$id%>_<%$field%>"
+                   SIZE  = "<% $size{$field} || 14 %>"
+                   VALUE = "<% scalar($cgi->param($name."_$field"))
+                               || $value |h %>"
+                   <% $onchange %>
+            >
+%         }
+          <BR>
           <FONT SIZE="-1"><% $label{$field} %></FONT>
         </TD>
 %     }
@@ -64,6 +80,9 @@
 
 my( %opt ) = @_;
 
+my $conf = new FS::Conf;
+my $self_base_url = $conf->config('selfservice_server-base_url');
+
 my $name = $opt{'element_name'} || $opt{'field'} || 'contactnum';
 my $id = $opt{'id'} || 'contactnum';
 
@@ -90,10 +109,11 @@ if ( $curr_value ) {
 my %size = ( 'title' => 12 );
 
 tie my %label, 'Tie::IxHash',
-  'first'        => 'First name',
-  'last'         => 'Last name',
-  'title'        => 'Title/Position',
-  'emailaddress' => 'Email',
+  'first'              => 'First name',
+  'last'               => 'Last name',
+  'title'              => 'Title/Position',
+  'emailaddress'       => 'Email',
+  'selfservice_access' => 'Self-service'
 ;
 
 my $first = 0;
diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html
index a851d99..f73483a 100644
--- a/httemplate/view/cust_main/contacts_new.html
+++ b/httemplate/view/cust_main/contacts_new.html
@@ -1,31 +1,56 @@
 <BR>
 <FONT CLASS="fsinnerbox-title">Contacts</FONT>
 <A HREF="<%$p%>edit/cust_main-contacts.html?<% $cust_main->custnum %>">Edit contacts</A>
-<TABLE CLASS="fsinnerbox">
+
+<& /elements/table-grid.html &>
+% my $bgcolor1 = '#eeeeee';
+%     my $bgcolor2 = '#ffffff';
+%     my $bgcolor = $bgcolor2;
+<TR>
+  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Type</TH>
+  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Contact</TH>
+  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Email</TH>
+  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Self-service</TH>
+% foreach my $phone_type (@phone_type) {
+    <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc"><% $phone_type->typename |h %> phone</TD>
+% }
+</TR>
+
 %   foreach my $contact ( @contacts ) {
-%     #XXX maybe this should be a table with alternating colors instead
       <TR>
-        <TD ALIGN="right"><% $contact->contact_classname %> Contact</TD>
-        <TD BGCOLOR="#FFFFFF"><% $contact->line %></TD>
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact->contact_classname |h %></TD>
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact->line |h %></TD>
 
 %       my @contact_email = $contact->contact_email;
-%       if (@contact_email) {
-          <TD ALIGN="right">   Email</TD>
-          <TD BGCOLOR="#FFFFFF"><% join(', ', map $_->emailaddress, @contact_email) %></TD>
-%       }
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% join(', ', map $_->emailaddress, @contact_email) %></TD>
+
+        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+%         if ( $contact->selfservice_access ) {
+            Enabled
+%#            <FONT SIZE="-1"><A HREF="XXX">disable</A>
+%#                            <A HREF="XXX">re-email</A></FONT>
+%         } else {
+            Disabled
+%#            <FONT SIZE="-1"><A HREF="XXX">enable</A></FONT>
+%        }
+       </TD>
 
 %       foreach my $phone_type (@phone_type) {
 %         my $contact_phone =
 %           qsearchs('contact_phone', {
 %                      'contactnum'   => $contact->contactnum,
 %                      'phonetypenum' => $phone_type->phonetypenum,
-%                   })
-%           or next;
-          <TD ALIGN="right">   <% $phone_type->typename %> phone</TD>
-          <TD BGCOLOR="#FFFFFF"><% $contact_phone->phonenum_pretty |h %></TD>
+%                   });
+          <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact_phone ? $contact_phone->phonenum_pretty : '' |h %></TD>
 %       }
 
       </TR>
+
+%     if ( $bgcolor eq $bgcolor1 ) {
+%        $bgcolor = $bgcolor2;
+%      } else {
+%        $bgcolor = $bgcolor1;
+%      }
 %   }
 </TABLE>
 <%once>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/ClientAPI/MyAccount.pm                       |  312 +++++++++++++++-----
 FS/FS/Conf.pm                                      |    3 +-
 FS/FS/Setup.pm                                     |    3 +
 FS/FS/contact.pm                                   |  188 ++++++++++++-
 FS/FS/msg_template.pm                              |   41 +++
 .../FS-SelfService/cgi/do_forgot_password.html     |   18 ++
 .../cgi/do_process_forgot_password.html            |   18 ++
 .../FS-SelfService/cgi/forgot_password.html        |   53 ++++
 fs_selfservice/FS-SelfService/cgi/login.html       |   21 +-
 .../cgi/process_forgot_password.html               |   44 +++
 fs_selfservice/FS-SelfService/cgi/selfservice.cgi  |  116 +++++---
 httemplate/edit/cust_main-contacts.html            |    4 +-
 httemplate/edit/msg_template.html                  |    1 +
 httemplate/edit/process/cust_main-contacts.html    |    4 +-
 httemplate/elements/contact.html                   |   44 ++-
 httemplate/view/cust_main/contacts_new.html        |   49 +++-
 16 files changed, 769 insertions(+), 150 deletions(-)
 create mode 100644 fs_selfservice/FS-SelfService/cgi/do_forgot_password.html
 create mode 100644 fs_selfservice/FS-SelfService/cgi/do_process_forgot_password.html
 create mode 100644 fs_selfservice/FS-SelfService/cgi/forgot_password.html
 create mode 100644 fs_selfservice/FS-SelfService/cgi/process_forgot_password.html




More information about the freeside-commits mailing list