diff options
author | Matthew Somerville <matthew-github@dracos.co.uk> | 2017-09-20 14:36:12 +0100 |
---|---|---|
committer | Matthew Somerville <matthew-github@dracos.co.uk> | 2017-09-30 15:04:06 +0100 |
commit | d0ae2a420905dbd0b79141d88e2c47956d1d65b2 (patch) | |
tree | c6d7abbb7a33fef2fa3af9c5684d61a6adf0eb1f | |
parent | bfdae700a840b74595bb4798ae6d50bb9172fa72 (diff) |
Add ability to log in on /auth via text.
A confirmation code is sent via Twilio to be entered on the site.
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 ‘sign in by email’ 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 ‘sign in by email’ 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 ‘No’ 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’t a valid link') %]</h1> + <p>[% loc('The link might have expired, or maybe you didn’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’t the correct code') %]</h1> + <p>[% loc('Try again') %]:</p> + [% ELSE %] + <h1>[% loc("Nearly done! Now check your phone…") %]</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 ‘sign in by email’ 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 ‘No’ 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 Binary files differnew file mode 100644 index 000000000..0fab32f8a --- /dev/null +++ b/web/cobrands/fixmystreet/images/phone-in-circle-100px.png 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; |