aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--conf/general.yml-example7
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm113
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Phone.pm96
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Profile.pm2
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm2
-rw-r--r--perllib/FixMyStreet/SMS.pm110
-rw-r--r--perllib/FixMyStreet/TestMech.pm2
-rw-r--r--t/Mock/Twilio.pm20
-rw-r--r--t/app/controller/auth.t40
-rw-r--r--t/app/controller/auth_phone.t95
-rw-r--r--t/app/controller/auth_profile.t6
-rw-r--r--t/app/controller/auth_social.t8
-rw-r--r--t/app/controller/dashboard.t4
-rw-r--r--templates/web/base/auth/change_email.html10
-rw-r--r--templates/web/base/auth/general.html39
-rw-r--r--templates/web/base/auth/smsform.html34
-rw-r--r--templates/web/base/js/translation_strings.html4
-rw-r--r--templates/web/zurich/auth/general.html30
-rw-r--r--web/cobrands/fixmystreet/fixmystreet.js2
-rw-r--r--web/cobrands/fixmystreet/images/phone-in-circle-100px.pngbin0 -> 2149 bytes
-rw-r--r--web/cobrands/sass/_base.scss6
22 files changed, 522 insertions, 109 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ea73749e..fec83feae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
* Unreleased
- New features:
+ - Optional logging in using confirmation by phone text.
- Area summary statistics page in admin #1834
- Bugfixes
- Shortlist menu item always remains a link #1855
diff --git a/conf/general.yml-example b/conf/general.yml-example
index 345a6426d..528fc1012 100644
--- a/conf/general.yml-example
+++ b/conf/general.yml-example
@@ -205,6 +205,13 @@ TESTING_COUNCILS: ''
# if you're using Message Manager, include the URL here (see https://github.com/mysociety/message-manager/)
MESSAGE_MANAGER_URL: ''
+# If you enable login via SMS authentication, you'll need a twilio account
+SMS_AUTHENTICATION: 0
+PHONE_COUNTRY: ''
+TWILIO_ACCOUNT_SID: ''
+TWILIO_AUTH_TOKEN: ''
+TWILIO_FROM_PARAMETER: ''
+
# If you want to hide all pages from non-logged-in users, set this to 1.
LOGIN_REQUIRED: 0
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index 3eb724ddd..a6a6378da 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -8,6 +8,7 @@ use Email::Valid;
use Digest::HMAC_SHA1 qw(hmac_sha1);
use JSON::MaybeXS;
use MIME::Base64;
+use FixMyStreet::SMS;
=head1 NAME
@@ -35,19 +36,19 @@ sub general : Path : Args(0) {
# all done unless we have a form posted to us
return unless $c->req->method eq 'POST';
- my $clicked_email = $c->get_param('email_sign_in');
- my $data_address = $c->get_param('email');
+ my $clicked_sign_in_by_code = $c->get_param('sign_in_by_code');
+ my $data_username = $c->get_param('username');
my $data_password = $c->get_param('password_sign_in');
my $data_email = $c->get_param('name') || $c->get_param('password_register');
# decide which action to take
- $c->detach('email_sign_in') if $clicked_email || ($data_email && !$data_password);
- if (!$data_address && !$data_password && !$data_email) {
+ $c->detach('code_sign_in') if $clicked_sign_in_by_code || ($data_email && !$data_password);
+ if (!$data_username && !$data_password && !$data_email) {
$c->detach('social/facebook_sign_in') if $c->get_param('facebook_sign_in');
$c->detach('social/twitter_sign_in') if $c->get_param('twitter_sign_in');
}
- $c->forward( 'sign_in' )
+ $c->forward( 'sign_in', [ $data_username ] )
&& $c->detach( 'redirect_on_signin', [ $c->get_param('r') ] );
}
@@ -57,6 +58,13 @@ sub general_test : Path('_test_') : Args(0) {
$c->stash->{template} = 'auth/token.html';
}
+sub authenticate : Private {
+ my ($self, $c, $type, $username, $password) = @_;
+ return 1 if $type eq 'email' && $c->authenticate({ email => $username, email_verified => 1, password => $password });
+ return 1 if FixMyStreet->config('SMS_AUTHENTICATION') && $type eq 'phone' && $c->authenticate({ phone => $username, phone_verified => 1, password => $password });
+ return 0;
+}
+
=head2 sign_in
Allow the user to sign in with a username and a password.
@@ -64,21 +72,18 @@ Allow the user to sign in with a username and a password.
=cut
sub sign_in : Private {
- my ( $self, $c, $email ) = @_;
+ my ( $self, $c, $username ) = @_;
- $email ||= $c->get_param('email') || '';
- $email = lc $email;
+ $username ||= '';
my $password = $c->get_param('password_sign_in') || '';
my $remember_me = $c->get_param('remember_me') || 0;
# Sign out just in case
$c->logout();
- if ( $email
- && $password
- && $c->authenticate( { email => $email, email_verified => 1, password => $password } ) )
- {
+ my $parsed = FixMyStreet::SMS->parse_username($username);
+ if ($parsed->{username} && $password && $c->forward('authenticate', [ $parsed->{type}, $parsed->{username}, $password ])) {
# unless user asked to be remembered limit the session to browser
$c->set_session_cookie_expire(0)
unless $remember_me;
@@ -91,25 +96,40 @@ sub sign_in : Private {
$c->stash(
sign_in_error => 1,
- email => $email,
+ username => $username,
remember_me => $remember_me,
);
return;
}
-=head2 email_sign_in
+=head2 code_sign_in
+
+Either email the user a link to sign in, or send an SMS token to do so.
-Email the user the details they need to sign in. Don't check for an account - if
-there isn't one we can create it when they come back with a token (which
-contains the email address).
+Don't check for an account - if there isn't one we can create it when
+they come back with a token (which contains the email/phone).
=cut
-sub email_sign_in : Private {
+sub code_sign_in : Private {
my ( $self, $c ) = @_;
+ my $username = $c->get_param('username') || '';
+
+ my $parsed = FixMyStreet::SMS->parse_username($username);
+
+ if ($parsed->{type} eq 'phone' && FixMyStreet->config('SMS_AUTHENTICATION')) {
+ $c->forward('phone/sign_in', [ $parsed->{phone} ]);
+ } else {
+ $c->forward('email_sign_in', [ $parsed->{username} ]);
+ }
+}
+
+sub email_sign_in : Private {
+ my ( $self, $c, $email ) = @_;
+
# check that the email is valid - otherwise flag an error
- my $raw_email = lc( $c->get_param('email') || '' );
+ my $raw_email = lc( $email || '' );
my $email_checker = Email::Valid->new(
-mxcheck => 1,
@@ -119,9 +139,8 @@ sub email_sign_in : Private {
my $good_email = $email_checker->address($raw_email);
if ( !$good_email ) {
- $c->stash->{email} = $raw_email;
- $c->stash->{email_error} =
- $raw_email ? $email_checker->details : 'missing';
+ $c->stash->{username} = $raw_email;
+ $c->stash->{username_error} = $raw_email ? $email_checker->details : 'missing_email';
return;
}
@@ -130,7 +149,7 @@ sub email_sign_in : Private {
# NB this uses the same template as a successful sign in to stop
# enumeration of valid email addresses.
if ( FixMyStreet->config('SIGNUPS_DISABLED')
- && !$c->model('DB::User')->search({ email => $good_email })->count
+ && !$c->model('DB::User')->find({ email => $good_email })
&& !$c->stash->{current_user} # don't break the change email flow
) {
$c->stash->{template} = 'auth/token.html';
@@ -168,6 +187,20 @@ sub email_sign_in : Private {
$c->stash->{template} = 'auth/token.html';
}
+sub get_token : Private {
+ my ( $self, $c, $token, $scope ) = @_;
+
+ $c->stash->{token_not_found} = 1, return unless $token;
+
+ my $token_obj = $c->model('DB::Token')->find({ scope => $scope, token => $token });
+
+ $c->stash->{token_not_found} = 1, return unless $token_obj;
+ $c->stash->{token_not_found} = 1, return if $token_obj->created < DateTime->now->subtract( days => 1 );
+
+ my $data = $token_obj->data;
+ return $data;
+}
+
=head2 token
Handle the 'email_sign_in' tokens. Find the account for the email address
@@ -178,35 +211,21 @@ Handle the 'email_sign_in' tokens. Find the account for the email address
sub token : Path('/M') : Args(1) {
my ( $self, $c, $url_token ) = @_;
- # retrieve the token or return
- my $token_obj = $url_token
- ? $c->model('DB::Token')->find( {
- scope => 'email_sign_in', token => $url_token
- } )
- : undef;
+ my $data = $c->forward('get_token', [ $url_token, 'email_sign_in' ]) || return;
- if ( !$token_obj ) {
- $c->stash->{token_not_found} = 1;
- return;
- }
+ $c->stash->{token_not_found} = 1, return
+ if $data->{old_email} && (!$c->user_exists || $c->user->email ne $data->{old_email});
- if ( $token_obj->created < DateTime->now->subtract( days => 1 ) ) {
- $c->stash->{token_not_found} = 1;
- return;
- }
-
- # find or create the user related to the token.
- my $data = $token_obj->data;
+ $c->detach( '/auth/process_login', [ $data, 'email' ] );
+}
- if ($data->{old_email} && (!$c->user_exists || $c->user->email ne $data->{old_email})) {
- $c->stash->{token_not_found} = 1;
- return;
- }
+sub process_login : Private {
+ my ( $self, $c, $data, $type ) = @_;
# sign out in case we are another user
$c->logout();
- my $user = $c->model('DB::User')->find_or_new({ email => $data->{email} });
+ my $user = $c->model('DB::User')->find_or_new({ $type => $data->{$type} });
# Bail out if this is a new user and SIGNUPS_DISABLED is set
$c->detach( '/page_error_403_access_denied', [] )
@@ -233,7 +252,7 @@ sub token : Path('/M') : Args(1) {
$user->facebook_id( $data->{facebook_id} ) if $data->{facebook_id};
$user->twitter_id( $data->{twitter_id} ) if $data->{twitter_id};
$user->update_or_insert;
- $c->authenticate( { email => $user->email, email_verified => 1 }, 'no_password' );
+ $c->authenticate( { $type => $data->{$type}, "${type}_verified" => 1 }, 'no_password' );
# send the user to their page
$c->detach( 'redirect_on_signin', [ $data->{r}, $data->{p} ] );
@@ -341,7 +360,7 @@ sub ajax_sign_in : Path('ajax/sign_in') {
my ( $self, $c ) = @_;
my $return = {};
- if ( $c->forward( 'sign_in' ) ) {
+ if ( $c->forward( 'sign_in', [ $c->get_param('email') ] ) ) {
$return->{name} = $c->user->name;
} else {
$return->{error} = 1;
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Phone.pm b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm
new file mode 100644
index 000000000..4f9a72594
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm
@@ -0,0 +1,96 @@
+package FixMyStreet::App::Controller::Auth::Phone;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use FixMyStreet::SMS;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Auth::Phone - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Controller for phone SMS based authentication
+
+=head1 METHODS
+
+=head2 code
+
+Handle the submission of a code sent by text to a mobile number.
+
+=cut
+
+sub code : Path('') {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'auth/smsform.html';
+
+ my $token = $c->stash->{token} = $c->get_param('token');
+ my $code = $c->get_param('code') || '';
+
+ my $data = $c->forward('/auth/get_token', [ $token, 'phone_sign_in' ]) || return;
+
+ $c->stash->{incorrect_code} = 1, return if $data->{code} ne $code;
+
+ $c->detach( '/auth/process_login', [ $data, 'phone' ] );
+}
+
+=head2 sign_in
+
+When signing in with a mobile phone number, we are sent here.
+This sends a text to that number with a confirmation code,
+and sets up the token/etc to deal with the response.
+
+=cut
+
+sub sign_in : Private {
+ my ( $self, $c, $phone ) = @_;
+
+ unless ($phone) {
+ $c->stash->{username_error} = 'other_phone';
+ return;
+ }
+
+ unless ($phone->is_mobile) {
+ $c->stash->{username} = $c->get_param('username'); # What was entered
+ $c->stash->{username_error} = 'nonmobile';
+ return;
+ }
+
+ (my $number = $phone->format) =~ s/\s+//g;
+
+ if ( FixMyStreet->config('SIGNUPS_DISABLED')
+ && !$c->model('DB::User')->find({ phone => $number })
+ ) {
+ $c->stash->{template} = 'auth/token.html';
+ return;
+ }
+
+ my $user_params = {};
+ $user_params->{password} = $c->get_param('password_register')
+ if $c->get_param('password_register');
+ my $user = $c->model('DB::User')->new( $user_params );
+
+ my $token_data = {
+ phone => $number,
+ r => $c->get_param('r'),
+ name => $c->get_param('name'),
+ password => $user->password,
+ };
+
+ $c->forward('send_token', [ $token_data, 'phone_sign_in', $number ]);
+}
+
+sub send_token : Private {
+ my ( $self, $c, $token_data, $token_scope, $to ) = @_;
+
+ my $result = FixMyStreet::SMS->send_token($token_data, $token_scope, $to);
+ $c->stash->{token} = $result->{token};
+ $c->log->debug("Sending text containing code *$result->{random}*");
+ $c->stash->{template} = 'auth/smsform.html';
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
index 68c40f9dc..453b4a8a3 100644
--- a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
@@ -85,7 +85,7 @@ sub change_email : Path('/auth/change_email') {
$c->forward('/auth/check_csrf_token');
$c->stash->{current_user} = $c->user;
$c->stash->{email_template} = 'change_email.txt';
- $c->forward('/auth/email_sign_in');
+ $c->forward('/auth/email_sign_in', [ $c->get_param('email') ]);
}
__PACKAGE__->meta->make_immutable;
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index 3f940d838..c2fd2a377 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -785,7 +785,7 @@ sub process_user : Private {
# The user is trying to sign in. We only care about email from the params.
if ( $c->get_param('submit_sign_in') || $c->get_param('password_sign_in') ) {
- unless ( $c->forward( '/auth/sign_in' ) ) {
+ unless ( $c->forward( '/auth/sign_in', [ $email ] ) ) {
$c->stash->{field_errors}->{password} = _('There was a problem with your email/password combination. If you cannot remember your password, or do not have one, please fill in the &lsquo;sign in by email&rsquo; section of the form.');
return 1;
}
diff --git a/perllib/FixMyStreet/SMS.pm b/perllib/FixMyStreet/SMS.pm
new file mode 100644
index 000000000..ec9251a1a
--- /dev/null
+++ b/perllib/FixMyStreet/SMS.pm
@@ -0,0 +1,110 @@
+package FixMyStreet::SMS;
+
+use strict;
+use warnings;
+
+# use JSON::MaybeXS;
+use Moo;
+use Number::Phone::Lib;
+use WWW::Twilio::API;
+
+use FixMyStreet;
+use mySociety::EmailUtil qw(is_valid_email);
+use FixMyStreet::DB;
+
+has twilio => (
+ is => 'lazy',
+ default => sub {
+ WWW::Twilio::API->new(
+ AccountSid => FixMyStreet->config('TWILIO_ACCOUNT_SID'),
+ AuthToken => FixMyStreet->config('TWILIO_AUTH_TOKEN'),
+ utf8 => 1,
+ );
+ },
+);
+
+has from => (
+ is => 'lazy',
+ default => sub { FixMyStreet->config('TWILIO_FROM_PARAMETER') },
+);
+
+sub send_token {
+ my ($class, $token_data, $token_scope, $to) = @_;
+
+ # Random number between 10,000 and 75,535
+ my $random = 10000 + unpack('n', mySociety::Random::random_bytes(2, 1));
+ $token_data->{code} = $random;
+ my $token_obj = FixMyStreet::DB->resultset("Token")->create({
+ scope => $token_scope,
+ data => $token_data,
+ });
+ my $body = sprintf(_("Your verification code is %s"), $random);
+
+ my $result = $class->new->send(to => $to, body => $body);
+ return {
+ random => $random,
+ token => $token_obj->token,
+ result => $result,
+ };
+}
+
+sub send {
+ my ($self, %params) = @_;
+ my $output = $self->twilio->POST('Messages.json',
+ From => $self->from,
+ To => $params{to},
+ Body => $params{body},
+ );
+ # At present, we do nothing and assume sent okay.
+ # TODO add error checking
+ # my $data = decode_json($output->{content});
+ # if ($output->{code} != 200) {
+ # return { error => "$data->{message} ($data->{code})" };
+ # }
+ # return { success => $data->{sid} };
+}
+
+=head2 parse_username
+
+Given a string that might be an email address or a phone number,
+return what we think it is, and if it's valid one of those. Or
+undef if it's empty.
+
+=cut
+
+sub parse_username {
+ my ($class, $username) = @_;
+
+ return { type => 'email', username => $username } unless $username;
+
+ $username = lc $username;
+ $username =~ s/\s+//g;
+
+ return { type => 'email', email => $username, username => $username } if is_valid_email($username);
+
+ my $type = $username =~ /^[^a-z]+$/i ? 'phone' : 'email';
+ my $phone = do {
+ if ($username =~ /^\+/) {
+ # If already in international format, use that
+ Number::Phone::Lib->new($username)
+ } else {
+ # Otherwise, assume it is country configured
+ my $country = FixMyStreet->config('PHONE_COUNTRY');
+ Number::Phone::Lib->new($country, $username);
+ }
+ };
+
+ if ($phone) {
+ $type = 'phone';
+ # Store phone without spaces
+ ($username = $phone->format) =~ s/\s+//g;
+ }
+
+ return {
+ type => $type,
+ phone => $phone,
+ username => $username,
+ };
+}
+
+1;
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index 406382f1d..20f7a3ace 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -95,7 +95,7 @@ sub log_in_ok {
# log in
$mech->get_ok('/auth');
$mech->submit_form_ok(
- { with_fields => { email => $email, password_sign_in => 'secret' } },
+ { with_fields => { username => $email, password_sign_in => 'secret' } },
"sign in using form" );
$mech->logged_in_ok;
diff --git a/t/Mock/Twilio.pm b/t/Mock/Twilio.pm
new file mode 100644
index 000000000..b35166704
--- /dev/null
+++ b/t/Mock/Twilio.pm
@@ -0,0 +1,20 @@
+package t::Mock::Twilio;
+
+use Web::Simple;
+
+has texts => (
+ is => 'ro',
+ default => sub { [] },
+);
+
+sub dispatch_request {
+ my $self = shift;
+
+ sub (POST + /2010-04-01/Accounts/*/Messages.json + %*) {
+ my ($self, $sid, $data) = @_;
+ push @{$self->texts}, $data;
+ return [ 200, [ 'Content-Type' => 'application/json' ], [ '{}' ] ];
+ },
+}
+
+__PACKAGE__->run_if_script;
diff --git a/t/app/controller/auth.t b/t/app/controller/auth.t
index 8cdf93227..661f99412 100644
--- a/t/app/controller/auth.t
+++ b/t/app/controller/auth.t
@@ -40,8 +40,8 @@ for my $test (
$mech->submit_form_ok(
{
form_name => 'general_auth',
- fields => { email => $email, },
- button => 'email_sign_in',
+ fields => { username => $email, },
+ button => 'sign_in_by_code',
},
"try to create an account with email '$email'"
);
@@ -59,8 +59,8 @@ $mech->get_ok('/auth');
$mech->submit_form_ok(
{
form_name => 'general_auth',
- fields => { email => $test_email, password_register => $test_password },
- button => 'email_sign_in',
+ fields => { username => $test_email, password_register => $test_password },
+ button => 'sign_in_by_code',
},
"create an account for '$test_email'"
);
@@ -107,11 +107,11 @@ foreach my $remember_me ( '1', '0' ) {
{
form_name => 'general_auth',
fields => {
- email => $test_email,
+ username => $test_email,
password_sign_in => $test_password,
remember_me => ( $remember_me ? 1 : undef ),
},
- button => 'sign_in',
+ button => 'sign_in_by_password',
},
"sign in with '$test_email' & '$test_password'"
);
@@ -133,15 +133,15 @@ $mech->submit_form_ok(
{
form_name => 'general_auth',
fields => {
- email => $test_email,
+ username => $test_email,
password_sign_in => 'not the password',
},
- button => 'sign_in',
+ button => 'sign_in_by_password',
},
"sign in with '$test_email' & 'not the password'"
);
is $mech->uri->path, '/auth', "redirected to correct page";
-$mech->content_contains( 'problem with your email/password combination', 'found error message' );
+$mech->content_contains( 'problem with your login information', 'found error message' );
subtest "sign in but have email form autofilled" => sub {
$mech->get_ok('/auth');
@@ -149,11 +149,11 @@ subtest "sign in but have email form autofilled" => sub {
{
form_name => 'general_auth',
fields => {
- email => $test_email,
+ username => $test_email,
password_sign_in => $test_password,
name => 'Auto-completed from elsewhere',
},
- button => 'sign_in',
+ button => 'sign_in_by_password',
},
"sign in with '$test_email' and auto-completed name"
);
@@ -169,10 +169,10 @@ subtest "sign in with uppercase email" => sub {
{
form_name => 'general_auth',
fields => {
- email => $uc_test_email,
+ username => $uc_test_email,
password_sign_in => $test_password,
},
- button => 'sign_in',
+ button => 'sign_in_by_password',
},
"sign in with '$uc_test_email' and auto-completed name"
);
@@ -197,8 +197,8 @@ FixMyStreet::override_config {
$mech->submit_form_ok(
{
form_name => 'general_auth',
- fields => { email => $test_email3, },
- button => 'email_sign_in',
+ fields => { username => $test_email3, },
+ button => 'sign_in_by_code',
},
"create a new account"
);
@@ -218,13 +218,13 @@ FixMyStreet::override_config {
{
form_name => 'general_auth',
fields => {
- email => "$test_email",
+ username => "$test_email",
password_register => $new_password,
r => 'faq', # Just as a test
},
- button => 'email_sign_in',
+ button => 'sign_in_by_code',
},
- "email_sign_in with '$test_email'"
+ "sign_in_by_code with '$test_email'"
);
$mech->not_logged_in_ok;
@@ -241,10 +241,10 @@ FixMyStreet::override_config {
{
form_name => 'general_auth',
fields => {
- email => $test_email,
+ username => $test_email,
password_sign_in => $new_password,
},
- button => 'sign_in',
+ button => 'sign_in_by_password',
},
"sign in with '$test_email' and new password"
);
diff --git a/t/app/controller/auth_phone.t b/t/app/controller/auth_phone.t
new file mode 100644
index 000000000..a2f8f9cac
--- /dev/null
+++ b/t/app/controller/auth_phone.t
@@ -0,0 +1,95 @@
+use FixMyStreet::TestMech;
+
+use t::Mock::Twilio;
+
+my $twilio = t::Mock::Twilio->new;
+LWP::Protocol::PSGI->register($twilio->to_psgi_app, host => 'api.twilio.com');
+
+my $mech = FixMyStreet::TestMech->new;
+
+subtest 'Log in with invalid number, fail' => sub {
+ FixMyStreet::override_config {
+ SMS_AUTHENTICATION => 1,
+ PHONE_COUNTRY => 'GB',
+ TWILIO_ACCOUNT_SID => 'AC123',
+ }, sub {
+ $mech->get_ok('/auth');
+ $mech->submit_form_ok({
+ form_name => 'general_auth',
+ fields => { username => '01214960000000' },
+ button => 'sign_in_by_code',
+ }, "sign in using bad number");
+ $mech->content_contains('Please check your phone number is correct');
+ };
+};
+
+subtest 'Log in using landline, fail' => sub {
+ FixMyStreet::override_config {
+ SMS_AUTHENTICATION => 1,
+ PHONE_COUNTRY => 'GB',
+ TWILIO_ACCOUNT_SID => 'AC123',
+ }, sub {
+ $mech->get_ok('/auth');
+ $mech->submit_form_ok({
+ form_name => 'general_auth',
+ fields => { username => '01214960000' },
+ button => 'sign_in_by_code',
+ }, "sign in using landline");
+ $mech->content_contains('Please enter a mobile number');
+ };
+};
+
+subtest 'Log in using mobile, by text' => sub {
+ FixMyStreet::override_config {
+ SMS_AUTHENTICATION => 1,
+ PHONE_COUNTRY => 'GB',
+ TWILIO_ACCOUNT_SID => 'AC123',
+ }, sub {
+ $mech->submit_form_ok({
+ form_name => 'general_auth',
+ fields => { username => '+61491570156', password_register => 'secret' },
+ button => 'sign_in_by_code',
+ }, "sign in using mobile");
+
+ $mech->submit_form_ok({
+ with_fields => { code => '00000' }
+ }, 'submit incorrect code');
+ $mech->content_contains('Try again');
+
+ my $text = shift @{$twilio->texts};
+ my ($code) = $text->{Body} =~ /(\d+)/;
+ $mech->submit_form_ok({
+ with_fields => { code => $code }
+ }, 'submit correct code');
+
+ my $user = FixMyStreet::App->model('DB::User')->find( { phone => '+61491570156' } );
+ ok $user, "user created";
+ is $mech->uri->path, '/my', "redirected to the 'my' section of site";
+ $mech->logged_in_ok;
+ $mech->log_out_ok;
+ };
+};
+
+subtest 'Log in using mobile, by password' => sub {
+ FixMyStreet::override_config {
+ SMS_AUTHENTICATION => 1,
+ }, sub {
+ $mech->get_ok('/auth');
+ $mech->submit_form_ok({
+ form_name => 'general_auth',
+ fields => { username => '+61491570156', password_sign_in => 'incorrect' },
+ button => 'sign_in_by_password',
+ }, "sign in using wrong password");
+ $mech->content_contains('There was a problem');
+ $mech->submit_form_ok({
+ form_name => 'general_auth',
+ fields => { username => '+61491570156', password_sign_in => 'secret' },
+ button => 'sign_in_by_password',
+ }, "sign in using password");
+
+ is $mech->uri->path, '/my', "redirected to the 'my' section of site";
+ $mech->logged_in_ok;
+ };
+};
+
+done_testing();
diff --git a/t/app/controller/auth_profile.t b/t/app/controller/auth_profile.t
index 883dc2003..2472564e8 100644
--- a/t/app/controller/auth_profile.t
+++ b/t/app/controller/auth_profile.t
@@ -17,12 +17,12 @@ END {
{
form_name => 'general_auth',
fields => {
- email => "$test_email",
+ username => "$test_email",
r => 'faq', # Just as a test
},
- button => 'email_sign_in',
+ button => 'sign_in_by_code',
},
- "email_sign_in with '$test_email'"
+ "sign_in_by_code with '$test_email'"
);
# follow link and change password - check not prompted for old password
diff --git a/t/app/controller/auth_social.t b/t/app/controller/auth_social.t
index 726d264bd..d16a0102e 100644
--- a/t/app/controller/auth_social.t
+++ b/t/app/controller/auth_social.t
@@ -104,8 +104,10 @@ for my $fb_state ( 'refused', 'no email', 'existing UID', 'okay' ) {
# and the ID carries through the confirmation
if ($page eq 'update') {
$fields->{rznvy} = $fb_email;
- } else {
+ } elsif ($page eq 'report') {
$fields->{email} = $fb_email;
+ } else {
+ $fields->{username} = $fb_email;
}
$fields->{name} = 'Ffion Tester';
$mech->submit_form(with_fields => $fields);
@@ -216,8 +218,10 @@ for my $tw_state ( 'refused', 'existing UID', 'no email' ) {
# and the ID carries through the confirmation
if ($page eq 'update') {
$fields->{rznvy} = $tw_email;
- } else {
+ } elsif ($page eq 'report') {
$fields->{email} = $tw_email;
+ } else {
+ $fields->{username} = $tw_email;
}
$fields->{name} = 'Ffion Tester';
$mech->submit_form(with_fields => $fields);
diff --git a/t/app/controller/dashboard.t b/t/app/controller/dashboard.t
index 457eceade..14bd76c41 100644
--- a/t/app/controller/dashboard.t
+++ b/t/app/controller/dashboard.t
@@ -31,7 +31,7 @@ FixMyStreet::override_config {
$mech->content_contains( 'sign in' );
$mech->submit_form(
- with_fields => { email => $test_user, password_sign_in => $test_pass }
+ with_fields => { username => $test_user, password_sign_in => $test_pass }
);
is $mech->status, '404', 'If not council user get 404';
@@ -42,7 +42,7 @@ FixMyStreet::override_config {
$mech->log_out_ok;
$mech->get_ok('/dashboard');
$mech->submit_form_ok( {
- with_fields => { email => $test_user, password_sign_in => $test_pass }
+ with_fields => { username => $test_user, password_sign_in => $test_pass }
} );
$mech->content_contains( 'Area 2651' );
diff --git a/templates/web/base/auth/change_email.html b/templates/web/base/auth/change_email.html
index 0f0e0a3bb..6a0d6aed4 100644
--- a/templates/web/base/auth/change_email.html
+++ b/templates/web/base/auth/change_email.html
@@ -12,14 +12,14 @@
<input type="hidden" name="token" value="[% csrf_token %]">
<fieldset>
- [% IF email_error;
+ [% IF username_error;
errors = {
- missing = loc('Please enter your email'),
- other = loc('Please check your email address is correct')
+ missing_email = loc('Please enter your email'),
+ other_email = loc('Please check your email address is correct')
};
- loc_email_error = errors.$email_error || errors.other;
+ loc_username_error = errors.$username_error || errors.other_email;
%]
- <div class="form-error">[% loc_email_error %]</div>
+ <div class="form-error">[% loc_username_error %]</div>
[% END %]
<div class="form-field">
diff --git a/templates/web/base/auth/general.html b/templates/web/base/auth/general.html
index aa8c6a871..e64230b41 100644
--- a/templates/web/base/auth/general.html
+++ b/templates/web/base/auth/general.html
@@ -36,25 +36,34 @@
<div id="js-social-email-hide">
[% END %]
- [% IF email_error;
+ [% IF username_error;
# other keys include fqdn, mxcheck if you'd like to write a custom error message
errors = {
- missing => loc('Please enter your email'),
- other => loc('Please check your email address is correct')
+ nonmobile = loc('Please enter a mobile number'),
+ missing_email = loc('Please enter your email'),
+ other_email = loc('Please check your email address is correct')
+ missing_phone = loc('Please enter your phone number'),
+ other_phone = loc('Please check your phone number is correct')
};
- loc_email_error = errors.$email_error || errors.other;
+ loc_username_error = errors.$username_error || errors.other_email;
END %]
- <label class="n" for="email">[% loc('Email') %]</label>
- [% IF loc_email_error %]
- <div class="form-error">[% loc_email_error %]</div>
+[% IF c.config.SMS_AUTHENTICATION %]
+ [% SET username_label = loc('Your email or mobile') %]
+[% ELSE %]
+ [% SET username_label = loc('Your email') %]
+[% END %]
+
+ <label class="n" for="username">[% username_label %]</label>
+ [% IF loc_username_error %]
+ <div class="form-error">[% loc_username_error %]</div>
[% ELSIF sign_in_error %]
- <div class="form-error">[% loc('There was a problem with your email/password combination. If you cannot remember your password, or do not have one, please fill in the &lsquo;sign in by email&rsquo; section of the form.') %]</div>
+ <div class="form-error">[% loc('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the &lsquo;No&rsquo; section of the form.') %]</div>
[% END %]
- <input type="email" class="form-control required email" id="email" name="email" value="[% email | html %]" placeholder="[% loc('Your email address') %]" autofocus>
+ <input type="text" class="form-control required" id="username" name="username" value="[% username | html %]" autofocus>
<div id="form_sign_in">
<h3>[% tprintf(loc("Do you have a %s password?", "%s is the site name"), site_name) %]</h3>
@@ -85,7 +94,7 @@
<div class="form-txt-submit-box">
<input type="password" name="password_sign_in" class="form-control" id="password_sign_in" value="" placeholder="[% loc('Your password') %]">
- <input class="green-btn" type="submit" name="sign_in" value="[% loc('Sign in') %]">
+ <input class="green-btn" type="submit" name="sign_in_by_password" value="[% loc('Sign in') %]">
</div>
<div class="checkbox-group">
@@ -95,7 +104,11 @@
<div class="general-notes">
<p><strong>[% loc('Forgotten your password?') %]</strong>
+ [% IF c.config.SMS_AUTHENTICATION %]
+ [% loc('Sign in by email or text, providing a new password. When you click the link in your email or enter the SMS authentication code, your password will be updated.') %]</p>
+ [% ELSE %]
[% loc('Sign in by email instead, providing a new password. When you click the link in your email, your password will be updated.') %]</p>
+ [% END %]
</div>
</div>
@@ -103,7 +116,11 @@
[% BLOCK form_sign_in_no %]
<div id="form_sign_in_no" class="form-box">
+ [% IF c.config.SMS_AUTHENTICATION %]
+ <h5>[% loc('<strong>No</strong> let me sign in by email or text') %]</h5>
+ [% ELSE %]
<h5>[% loc('<strong>No</strong> let me sign in by email') %]</h5>
+ [% END %]
<label for="name">[% loc('Name') %]</label>
<input class="form-control" type="text" name="name" value="" placeholder="[% loc('Your name') %]">
@@ -116,7 +133,7 @@
<div class="form-txt-submit-box">
<input class="form-control" type="password" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]">
- <input class="green-btn" type="submit" name="email_sign_in" value="[% loc('Sign in') %]">
+ <input class="green-btn" type="submit" name="sign_in_by_code" value="[% loc('Sign in') %]">
</div>
</div>
[% END %]
diff --git a/templates/web/base/auth/smsform.html b/templates/web/base/auth/smsform.html
new file mode 100644
index 000000000..a475dd2f6
--- /dev/null
+++ b/templates/web/base/auth/smsform.html
@@ -0,0 +1,34 @@
+[% INCLUDE 'header.html', bodyclass = 'fullwidthpage', title = loc('Confirm account') %]
+
+[% IF token_not_found %]
+
+ <div class="confirmation-header confirmation-header--failure">
+ <h1>[% loc('Sorry, that wasn&rsquo;t a valid link') %]</h1>
+ <p>[% loc('The link might have expired, or maybe you didn&rsquo;t quite copy and paste it correctly.') %]</p>
+ </div>
+
+[% ELSE %]
+
+[% DEFAULT submit_url = '/auth/phone' %]
+
+ <div class="confirmation-header confirmation-header--phone">
+ [% IF incorrect_code %]
+ <h1>[% loc('Sorry, that wasn&rsquo;t the correct code') %]</h1>
+ <p>[% loc('Try again') %]:</p>
+ [% ELSE %]
+ <h1>[% loc("Nearly done! Now check your phone&hellip;") %]</h1>
+ <p>[% loc("We have sent a confirmation code to your phone. Please enter it below:") %]</p>
+ [% END %]
+ <form action="[% submit_url %]" method="post">
+ <input type="hidden" name="token" value="[% token | html %]">
+ <label for="code">[% loc('Code') %]</label>
+ <div class="form-txt-submit-box">
+ <input class="form-control" type="number" id="code" name="code" value="" required>
+ <input type="submit" value="[% loc('Submit') %]" class="btn-primary">
+ </div>
+ </form>
+ </div>
+
+[% END %]
+
+[% INCLUDE 'footer.html' %]
diff --git a/templates/web/base/js/translation_strings.html b/templates/web/base/js/translation_strings.html
index bc2f013ff..9bdf3b498 100644
--- a/templates/web/base/js/translation_strings.html
+++ b/templates/web/base/js/translation_strings.html
@@ -59,7 +59,11 @@
upload_cancel_confirmation: '[% loc ('Are you sure you want to cancel this upload?') | replace("'", "\\'") %]',
upload_invalid_file_type: '[% loc ('Please upload an image only') | replace("'", "\\'") %]',
+ [% IF c.config.SMS_AUTHENTICATION ~%]
+ login_with_email: '[% loc('Log in with email/text') | replace("'", "\\'") %]',
+ [% ELSE ~%]
login_with_email: '[% loc('Log in with email') | replace("'", "\\'") %]',
+ [% END ~%]
offline: {
your_reports: '[% loc('Your offline reports') | replace("'", "\\'") %]',
diff --git a/templates/web/zurich/auth/general.html b/templates/web/zurich/auth/general.html
index 000cf3349..899f0ca71 100644
--- a/templates/web/zurich/auth/general.html
+++ b/templates/web/zurich/auth/general.html
@@ -1,15 +1,15 @@
[% INCLUDE 'header.html', title = loc('Sign in or create an account') %]
-[% IF email_error;
+[% IF username_error;
# other keys include fqdn, mxcheck if you'd like to write a custom error message
errors = {
- missing => loc('Please enter your email'),
- other => loc('Please check your email address is correct')
+ missing_email = loc('Please enter your email'),
+ other_email = loc('Please check your email address is correct')
};
- loc_email_error = errors.$email_error || errors.other;
+ loc_username_error = errors.$username_error || errors.other_email;
END %]
<form action="/auth" method="post" name="general_auth_login" class="validate">
@@ -21,18 +21,18 @@ END %]
<div id="form_sign_in_yes" class="form-box">
- <label class="n" for="email">[% loc('Email') %]</label>
- [% IF loc_email_error %]
- <div class="form-error">[% loc_email_error %]</div>
+ <label class="n" for="username">[% loc('Email') %]</label>
+ [% IF loc_username_error %]
+ <div class="form-error">[% loc_username_error %]</div>
[% ELSIF sign_in_error %]
- <div class="form-error">[% loc('There was a problem with your email/password combination. If you cannot remember your password, or do not have one, please fill in the &lsquo;sign in by email&rsquo; section of the form.') %]</div>
+ <div class="form-error">[% loc('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the &lsquo;No&rsquo; section of the form.') %]</div>
[% END %]
- <input type="email" class="required email" id="email" name="email" value="[% email | html %]" placeholder="[% loc('Your email address') %]" autofocus>
+ <input type="email" class="required email" id="username" name="username" value="[% username | html %]" placeholder="[% loc('Your email address') %]" autofocus>
<label for="password_sign_in">[% loc('Password (optional)') %]</label>
<div class="form-txt-submit-box">
<input type="password" class="required" name="password_sign_in" id="password_sign_in" value="" placeholder="[% loc('Your password') %]">
- <input class="green-btn" type="submit" name="sign_in" value="[% loc('Sign in') %]">
+ <input class="green-btn" type="submit" name="sign_in_by_password" value="[% loc('Sign in') %]">
</div>
<div class="form-txt-submit-box">
@@ -51,11 +51,11 @@ END %]
<h1>[% loc('<strong>No</strong> let me sign in by email') %]</h1>
<div id="form_sign_in_no" class="form-box">
- <label class="n" for="email2">[% loc('Email') %]</label>
- [% IF loc_email_error %]
- <div class="form-error">[% loc_email_error %]</div>
+ <label class="n" for="username2">[% loc('Email') %]</label>
+ [% IF loc_username_error %]
+ <div class="form-error">[% loc_username_error %]</div>
[% END %]
- <input type="email" class="required email" id="email2" name="email" value="[% email | html %]" placeholder="[% loc('Your email address') %]">
+ <input type="email" class="required email" id="username2" name="username" value="[% username | html %]" placeholder="[% loc('Your email address') %]">
<label for="name">[% loc('Name') %]</label>
<input type="text" class="required" name="name" value="" placeholder="[% loc('Your name') %]">
@@ -63,7 +63,7 @@ END %]
<label for="password_register">[% loc('Password (optional)') %]</label>
<div class="form-txt-submit-box">
<input type="password" class="required" name="password_register" id="password_register" value="" placeholder="[% loc('Enter a password') %]">
- <input class="green-btn" type="submit" name="email_sign_in" value="Registrieren">
+ <input class="green-btn" type="submit" name="sign_in_by_code" value="Registrieren">
</div>
</div>
diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js
index 8673b6b76..25d956d61 100644
--- a/web/cobrands/fixmystreet/fixmystreet.js
+++ b/web/cobrands/fixmystreet/fixmystreet.js
@@ -345,7 +345,7 @@ $.extend(fixmystreet.set_up, {
$('#facebook_sign_in, #twitter_sign_in').click(function(e){
$('#form_email').removeClass();
$('#form_rznvy').removeClass();
- $('#email').removeClass();
+ $('#username').removeClass();
});
$('#planned_form').submit(function(e) {
diff --git a/web/cobrands/fixmystreet/images/phone-in-circle-100px.png b/web/cobrands/fixmystreet/images/phone-in-circle-100px.png
new file mode 100644
index 000000000..0fab32f8a
--- /dev/null
+++ b/web/cobrands/fixmystreet/images/phone-in-circle-100px.png
Binary files differ
diff --git a/web/cobrands/sass/_base.scss b/web/cobrands/sass/_base.scss
index ce28badab..4cf18a09c 100644
--- a/web/cobrands/sass/_base.scss
+++ b/web/cobrands/sass/_base.scss
@@ -228,6 +228,7 @@ input[type=file] {
width: 100%;
}
+input[type=number],
input[type=text],
input[type=password],
input[type=email],
@@ -402,6 +403,7 @@ select.form-control {
.form-txt-submit-box {
@include clearfix();
input[type=password],
+ input[type=number],
input[type=text],
input[type=email] {
width: 65%;
@@ -2200,6 +2202,10 @@ table.nicetable {
background-image: url(/cobrands/fixmystreet/images/inbox-in-circle-100px.png);
}
+ &.confirmation-header--phone {
+ background-image: url(/cobrands/fixmystreet/images/phone-in-circle-100px.png);
+ }
+
h1, h2 {
margin: 0;
line-height: 1.2em;