aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet/App/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet/App/Controller')
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm332
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm385
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Phone.pm100
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Profile.pm151
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Social.pm203
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm28
-rw-r--r--perllib/FixMyStreet/App/Controller/My.pm4
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm224
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm185
-rw-r--r--perllib/FixMyStreet/App/Controller/Tokens.pm100
10 files changed, 1065 insertions, 647 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index a47e74f19..0caa25710 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -14,6 +14,7 @@ use List::MoreUtils 'uniq';
use mySociety::ArrayUtils;
use FixMyStreet::SendReport;
+use FixMyStreet::SMS;
=head1 NAME
@@ -678,6 +679,10 @@ sub reports : Path('reports') {
my $like_search = "%$search%";
+ my $parsed = FixMyStreet::SMS->parse_username($search);
+ my $valid_phone = $parsed->{phone};
+ my $valid_email = $parsed->{email};
+
# when DBIC creates the join it does 'JOIN users user' in the
# SQL which makes PostgreSQL unhappy as user is a reserved
# word. So look up user ID for email separately.
@@ -686,10 +691,19 @@ sub reports : Path('reports') {
}, { columns => [ 'id' ] } )->all;
@user_ids = map { $_->id } @user_ids;
- if (is_valid_email($search)) {
+ my @user_ids_phone = $c->model('DB::User')->search({
+ phone => { ilike => $like_search },
+ }, { columns => [ 'id' ] } )->all;
+ @user_ids_phone = map { $_->id } @user_ids_phone;
+
+ if ($valid_email) {
$query->{'-or'} = [
'me.user_id' => { -in => \@user_ids },
];
+ } elsif ($valid_phone) {
+ $query->{'-or'} = [
+ 'me.user_id' => { -in => \@user_ids_phone },
+ ];
} elsif ($search =~ /^id:(\d+)$/) {
$query->{'-or'} = [
'me.id' => int($1),
@@ -705,7 +719,7 @@ sub reports : Path('reports') {
} else {
$query->{'-or'} = [
'me.id' => $search_n,
- 'me.user_id' => { -in => \@user_ids },
+ 'me.user_id' => { -in => [ @user_ids, @user_ids_phone ] },
'me.external_id' => { ilike => $like_search },
'me.name' => { ilike => $like_search },
'me.title' => { ilike => $like_search },
@@ -726,10 +740,14 @@ sub reports : Path('reports') {
$c->stash->{problems} = [ $problems->all ];
$c->stash->{problems_pager} = $problems->pager;
- if (is_valid_email($search)) {
+ if ($valid_email) {
$query = [
'me.user_id' => { -in => \@user_ids },
];
+ } elsif ($valid_phone) {
+ $query = [
+ 'me.user_id' => { -in => \@user_ids_phone },
+ ];
} elsif ($search =~ /^id:(\d+)$/) {
$query = [
'me.id' => int($1),
@@ -741,7 +759,7 @@ sub reports : Path('reports') {
$query = [
'me.id' => $search_n,
'problem.id' => $search_n,
- 'me.user_id' => { -in => \@user_ids },
+ 'me.user_id' => { -in => [ @user_ids, @user_ids_phone ] },
'me.name' => { ilike => $like_search },
text => { ilike => $like_search },
'me.cobrand_data' => { ilike => $like_search },
@@ -834,7 +852,7 @@ sub report_edit : Path('report_edit') : Args(1) {
return if $done;
}
- $c->forward('check_email_for_abuse', [ $problem->user->email ] );
+ $c->forward('check_username_for_abuse', [ $problem->user ] );
$c->stash->{updates} =
[ $c->model('DB::Comment')
@@ -884,11 +902,12 @@ sub report_edit : Path('report_edit') : Args(1) {
$c->forward( '/admin/report_edit_category', [ $problem ] );
- my $email = lc $c->get_param('email');
- if ( $email ne $problem->user->email ) {
- my $user = $c->model('DB::User')->find_or_create({ email => $email });
- $user->insert unless $user->in_storage;
- $problem->user( $user );
+ my $parsed = FixMyStreet::SMS->parse_username($c->get_param('username'));
+ if ($parsed->{email} || ($parsed->{phone} && $parsed->{phone}->is_mobile)) {
+ my $user = $c->model('DB::User')->find_or_create({ $parsed->{type} => $parsed->{username} });
+ if ($user->id && $user->id != $problem->user->id) {
+ $problem->user( $user );
+ }
}
# Deal with photos
@@ -1145,28 +1164,15 @@ sub users: Path('users') : Args(0) {
{
-or => [
email => { ilike => $isearch },
+ phone => { ilike => $isearch },
name => { ilike => $isearch },
from_body => $search_n,
]
}
);
my @users = $users->all;
- my %email2user = map { $_->email => $_ } @users;
$c->stash->{users} = [ @users ];
-
- if ( $c->user->is_superuser ) {
- my $emails = $c->model('DB::Abuse')->search(
- { email => { ilike => $isearch } }
- );
- foreach my $email ($emails->all) {
- # Slight abuse of the boolean flagged value
- if ($email2user{$email->email}) {
- $email2user{$email->email}->flagged( 2 );
- } else {
- push @{$c->stash->{users}}, { email => $email->email, flagged => 2 };
- }
- }
- }
+ $c->forward('add_flags', [ { email => { ilike => $isearch } } ]);
} else {
$c->forward('/auth/get_csrf_token');
@@ -1178,9 +1184,7 @@ sub users: Path('users') : Args(0) {
{ order_by => 'name' }
);
my @users = $users->all;
- my %email2user = map { $_->email => $_ } @users;
$c->stash->{users} = \@users;
-
}
return 1;
@@ -1203,7 +1207,7 @@ sub update_edit : Path('update_edit') : Args(1) {
return 1;
}
- $c->forward('check_email_for_abuse', [ $update->user->email ] );
+ $c->forward('check_username_for_abuse', [ $update->user ] );
if ( $c->get_param('banuser') ) {
$c->forward('ban_user');
@@ -1227,9 +1231,7 @@ sub update_edit : Path('update_edit') : Args(1) {
# $update->name can be null which makes ne unhappy
my $name = $update->name || '';
- my $email = lc $c->get_param('email');
if ( $c->get_param('name') ne $name
- || $email ne $update->user->email
|| $c->get_param('anonymous') ne $update->anonymous
|| $c->get_param('text') ne $update->text ) {
$edited = 1;
@@ -1249,10 +1251,13 @@ sub update_edit : Path('update_edit') : Args(1) {
$update->anonymous( $c->get_param('anonymous') );
$update->state( $new_state );
- if ( $email ne $update->user->email ) {
- my $user = $c->model('DB::User')->find_or_create({ email => $email });
- $user->insert unless $user->in_storage;
- $update->user($user);
+ my $parsed = FixMyStreet::SMS->parse_username($c->get_param('username'));
+ if ($parsed->{email} || ($parsed->{phone} && $parsed->{phone}->is_mobile)) {
+ my $user = $c->model('DB::User')->find_or_create({ $parsed->{type} => $parsed->{username} });
+ if ($user->id && $user->id != $update->user->id) {
+ $edited = 1;
+ $update->user( $user );
+ }
}
if ( $new_state eq 'confirmed' and $old_state eq 'unconfirmed' ) {
@@ -1305,24 +1310,53 @@ sub user_add : Path('user_edit') : Args(0) {
$c->forward('/auth/check_csrf_token');
$c->stash->{field_errors} = {};
- unless ($c->get_param('email')) {
+ my $email = lc $c->get_param('email');
+ my $phone = $c->get_param('phone');
+ my $email_v = $c->get_param('email_verified');
+ my $phone_v = $c->get_param('phone_verified');
+
+ unless ($email || $phone) {
+ $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number');
+ }
+ if (!$email_v && !$phone_v) {
+ $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone');
+ }
+ if ($email && !is_valid_email($email)) {
$c->stash->{field_errors}->{email} = _('Please enter a valid email');
}
unless ($c->get_param('name')) {
$c->stash->{field_errors}->{name} = _('Please enter a name');
}
+
+ if ($phone_v) {
+ my $parsed = FixMyStreet::SMS->parse_username($phone);
+ if ($parsed->{phone} && $parsed->{phone}->is_mobile) {
+ $phone = $parsed->{username};
+ } elsif ($parsed->{phone}) {
+ $c->stash->{field_errors}->{phone} = _('Please enter a mobile number');
+ } else {
+ $c->stash->{field_errors}->{phone} = _('Please check your phone number is correct');
+ }
+ }
+
+ my $existing_email = $email_v && $c->model('DB::User')->find( { email => $email } );
+ my $existing_phone = $phone_v && $c->model('DB::User')->find( { phone => $phone } );
+ if ($existing_email || $existing_phone) {
+ $c->stash->{field_errors}->{username} = _('User already exists');
+ }
+
return if %{$c->stash->{field_errors}};
- my $user = $c->model('DB::User')->find_or_create( {
+ my $user = $c->model('DB::User')->create( {
name => $c->get_param('name'),
- email => lc $c->get_param('email'),
- phone => $c->get_param('phone') || undef,
+ email => $email ? $email : undef,
+ email_verified => $email && $email_v ? 1 : 0,
+ phone => $phone || undef,
+ phone_verified => $phone && $phone_v ? 1 : 0,
from_body => $c->get_param('body') || undef,
flagged => $c->get_param('flagged') || 0,
# Only superusers can create superusers
is_superuser => ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0,
- }, {
- key => 'users_email_key'
} );
$c->stash->{user} = $user;
$c->forward('user_cobrand_extra_fields');
@@ -1366,19 +1400,72 @@ sub user_edit : Path('user_edit') : Args(1) {
my $edited = 0;
my $email = lc $c->get_param('email');
- if ( $user->email ne $email ||
+ my $phone = $c->get_param('phone');
+ my $email_v = $c->get_param('email_verified') || 0;
+ my $phone_v = $c->get_param('phone_verified') || 0;
+
+ $c->stash->{field_errors} = {};
+
+ unless ($email || $phone) {
+ $c->stash->{field_errors}->{username} = _('Please enter a valid email or phone number');
+ }
+ if (!$email_v && !$phone_v) {
+ $c->stash->{field_errors}->{username} = _('Please verify at least one of email/phone');
+ }
+ if ($email && !is_valid_email($email)) {
+ $c->stash->{field_errors}->{email} = _('Please enter a valid email');
+ }
+
+ if ($phone_v) {
+ my $parsed = FixMyStreet::SMS->parse_username($phone);
+ if ($parsed->{phone} && $parsed->{phone}->is_mobile) {
+ $phone = $parsed->{username};
+ } elsif ($parsed->{phone}) {
+ $c->stash->{field_errors}->{phone} = _('Please enter a mobile number');
+ } else {
+ $c->stash->{field_errors}->{phone} = _('Please check your phone number is correct');
+ }
+ }
+
+ unless ($user->name) {
+ $c->stash->{field_errors}->{name} = _('Please enter a name');
+ }
+
+ my $email_params = { email => $email, email_verified => 1, id => { '!=', $user->id } };
+ my $phone_params = { phone => $phone, phone_verified => 1, id => { '!=', $user->id } };
+ my $existing_email = $email_v && $c->model('DB::User')->search($email_params)->first;
+ my $existing_phone = $phone_v && $c->model('DB::User')->search($phone_params)->first;
+ my $existing_user = $existing_email || $existing_phone;
+ my $existing_email_cobrand = $email_v && $c->cobrand->users->search($email_params)->first;
+ my $existing_phone_cobrand = $phone_v && $c->cobrand->users->search($phone_params)->first;
+ my $existing_user_cobrand = $existing_email_cobrand || $existing_phone_cobrand;
+ if ($existing_phone_cobrand && $existing_email_cobrand && $existing_email_cobrand->id != $existing_phone_cobrand->id) {
+ $c->stash->{field_errors}->{username} = _('User already exists');
+ }
+
+ return if %{$c->stash->{field_errors}};
+
+ if ( ($user->email || "") ne $email ||
$user->name ne $c->get_param('name') ||
- ($user->phone || "") ne $c->get_param('phone') ||
+ ($user->phone || "") ne $phone ||
($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) ||
(!$user->from_body && $c->get_param('body'))
) {
$edited = 1;
}
+ if ($existing_user_cobrand) {
+ $existing_user->adopt($user);
+ $c->forward( 'log_edit', [ $id, 'user', 'merge' ] );
+ return $c->res->redirect( $c->uri_for( 'user_edit', $existing_user->id ) );
+ }
+
+ $user->email($email) if !$existing_email;
+ $user->phone($phone) if !$existing_phone;
+ $user->email_verified( $email_v );
+ $user->phone_verified( $phone_v );
$user->name( $c->get_param('name') );
- my $original_email = $user->email;
- $user->email( $email );
- $user->phone( $c->get_param('phone') ) if $c->get_param('phone');
+
$user->flagged( $c->get_param('flagged') || 0 );
# Only superusers can grant superuser status
$user->is_superuser( ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0 );
@@ -1457,8 +1544,6 @@ sub user_edit : Path('user_edit') : Args(1) {
}
}
- $c->stash->{field_errors} = {};
-
# Update the categories this user operates in
if ( $user->from_body ) {
$c->stash->{body} = $user->from_body;
@@ -1469,33 +1554,12 @@ sub user_edit : Path('user_edit') : Args(1) {
$user->set_extra_metadata('categories', \@new_contact_ids);
}
- unless ($user->email) {
- $c->stash->{field_errors}->{email} = _('Please enter a valid email');
- }
- unless ($user->name) {
- $c->stash->{field_errors}->{name} = _('Please enter a name');
- }
- return if %{$c->stash->{field_errors}};
-
- my $existing_user = $c->model('DB::User')->search({ email => $user->email, id => { '!=', $user->id } })->first;
- my $existing_user_cobrand = $c->cobrand->users->search({ email => $user->email, id => { '!=', $user->id } })->first;
- if ($existing_user_cobrand) {
- $existing_user->adopt($user);
- $c->forward( 'log_edit', [ $id, 'user', 'merge' ] );
- $c->res->redirect( $c->uri_for( 'user_edit', $existing_user->id ) );
- } else {
- if ($existing_user) {
- # Tried to change email to an existing one lacking permission
- # so make sure it's switched back
- $user->email($original_email);
- }
- $user->update;
- if ($edited) {
- $c->forward( 'log_edit', [ $id, 'user', 'edit' ] );
- }
- $c->flash->{status_message} = _("Updated!");
- $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) );
+ $user->update;
+ if ($edited) {
+ $c->forward( 'log_edit', [ $id, 'user', 'edit' ] );
}
+ $c->flash->{status_message} = _("Updated!");
+ return $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) );
}
if ( $user->from_body ) {
@@ -1526,6 +1590,27 @@ sub user_cobrand_extra_fields : Private {
}
}
+sub add_flags : Private {
+ my ( $self, $c, $search ) = @_;
+
+ return unless $c->user->is_superuser;
+
+ my $users = $c->stash->{users};
+ my %email2user = map { $_->email => $_ } grep { $_->email } @$users;
+ my %phone2user = map { $_->phone => $_ } grep { $_->phone } @$users;
+ my %username2user = (%email2user, %phone2user);
+ my $usernames = $c->model('DB::Abuse')->search($search);
+
+ foreach my $username (map { $_->email } $usernames->all) {
+ # Slight abuse of the boolean flagged value
+ if ($username2user{$username}) {
+ $username2user{$username}->flagged( 2 );
+ } else {
+ push @{$c->stash->{users}}, { email => $username, flagged => 2 };
+ }
+ }
+}
+
sub flagged : Path('flagged') : Args(0) {
my ( $self, $c ) = @_;
@@ -1535,23 +1620,10 @@ sub flagged : Path('flagged') : Args(0) {
# which has to use an array ref for sql quoting reasons
$c->stash->{problems} = [ $problems->all ];
- my $users = $c->cobrand->users->search( { flagged => 1 } );
- my @users = $users->all;
- my %email2user = map { $_->email => $_ } @users;
+ my @users = $c->cobrand->users->search( { flagged => 1 } )->all;
$c->stash->{users} = [ @users ];
- my @abuser_emails = $c->model('DB::Abuse')->all()
- if $c->user->is_superuser;
-
- foreach my $email (@abuser_emails) {
- # Slight abuse of the boolean flagged value
- if ($email2user{$email->email}) {
- $email2user{$email->email}->flagged( 2 );
- } else {
- push @{$c->stash->{users}}, { email => $email->email, flagged => 2 };
- }
- }
-
+ $c->forward('add_flags', [ {} ]);
return 1;
}
@@ -1734,47 +1806,60 @@ sub log_edit : Private {
=head2 ban_user
-Add the email address in the email param of the request object to
-the abuse table if they are not already in there and sets status_message
-accordingly
+Add the user's email address/phone number to the abuse table if they are not
+already in there and sets status_message accordingly.
=cut
sub ban_user : Private {
my ( $self, $c ) = @_;
- my $email = lc $c->get_param('email');
-
- return unless $email;
-
- my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $email });
-
- if ( $abuse->in_storage ) {
- $c->stash->{status_message} = _('Email already in abuse list');
- } else {
- $abuse->insert;
- $c->stash->{status_message} = _('Email added to abuse list');
+ my $user;
+ if ($c->stash->{problem}) {
+ $user = $c->stash->{problem}->user;
+ } elsif ($c->stash->{update}) {
+ $user = $c->stash->{update}->user;
}
+ return unless $user;
- $c->stash->{email_in_abuse} = 1;
-
+ if ($user->email_verified && $user->email) {
+ my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->email });
+ if ( $abuse->in_storage ) {
+ $c->stash->{status_message} = _('User already in abuse list');
+ } else {
+ $abuse->insert;
+ $c->stash->{status_message} = _('User added to abuse list');
+ }
+ $c->stash->{username_in_abuse} = 1;
+ }
+ if ($user->phone_verified && $user->phone) {
+ my $abuse = $c->model('DB::Abuse')->find_or_new({ email => $user->phone });
+ if ( $abuse->in_storage ) {
+ $c->stash->{status_message} = _('User already in abuse list');
+ } else {
+ $abuse->insert;
+ $c->stash->{status_message} = _('User added to abuse list');
+ }
+ $c->stash->{username_in_abuse} = 1;
+ }
return 1;
}
=head2 flag_user
-Sets the flag on a user with the given email
+Sets the flag on a user
=cut
sub flag_user : Private {
my ( $self, $c ) = @_;
- my $email = lc $c->get_param('email');
-
- return unless $email;
-
- my $user = $c->cobrand->users->find({ email => $email });
+ my $user;
+ if ($c->stash->{problem}) {
+ $user = $c->stash->{problem}->user;
+ } elsif ($c->stash->{update}) {
+ $user = $c->stash->{update}->user;
+ }
if ( !$user ) {
$c->stash->{status_message} = _('Could not find user');
@@ -1791,18 +1876,19 @@ sub flag_user : Private {
=head2 remove_user_flag
-Remove the flag on a user with the given email
+Remove the flag on a user
=cut
sub remove_user_flag : Private {
my ( $self, $c ) = @_;
- my $email = lc $c->get_param('email');
-
- return unless $email;
-
- my $user = $c->cobrand->users->find({ email => $email });
+ my $user;
+ if ($c->stash->{problem}) {
+ $user = $c->stash->{problem}->user;
+ } elsif ($c->stash->{update}) {
+ $user = $c->stash->{update}->user;
+ }
if ( !$user ) {
$c->stash->{status_message} = _('Could not find user');
@@ -1816,20 +1902,20 @@ sub remove_user_flag : Private {
}
-=head2 check_email_for_abuse
+=head2 check_username_for_abuse
- $c->forward('check_email_for_abuse', [ $email ] );
+ $c->forward('check_username_for_abuse', [ $user ] );
-Checks if $email is in the abuse table and sets email_in_abuse accordingly
+Checks if $user is in the abuse table and sets username_in_abuse accordingly.
=cut
-sub check_email_for_abuse : Private {
- my ( $self, $c, $email ) =@_;
+sub check_username_for_abuse : Private {
+ my ( $self, $c, $user ) = @_;
- my $is_abuse = $c->model('DB::Abuse')->find({ email => $email });
+ my $is_abuse = $c->model('DB::Abuse')->find({ email => [ $user->phone, $user->email ] });
- $c->stash->{email_in_abuse} = 1 if $is_abuse;
+ $c->stash->{username_in_abuse} = 1 if $is_abuse;
return 1;
}
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index 825066026..b453f593b 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -5,12 +5,10 @@ use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
use Email::Valid;
-use Net::Domain::TLD;
use Digest::HMAC_SHA1 qw(hmac_sha1);
use JSON::MaybeXS;
use MIME::Base64;
-use Net::Facebook::Oauth2;
-use Net::Twitter::Lite::WithAPIv1_1;
+use FixMyStreet::SMS;
=head1 NAME
@@ -38,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('facebook_sign_in') if $c->get_param('facebook_sign_in');
- $c->detach('twitter_sign_in') if $c->get_param('twitter_sign_in');
+ $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') ] );
}
@@ -60,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.
@@ -67,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, 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;
@@ -94,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
-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).
+Either email the user a link to sign in, or send an SMS token to do so.
+
+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->stash->{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,
@@ -122,9 +139,7 @@ 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_error} = $raw_email ? $email_checker->details : 'missing_email';
return;
}
@@ -133,7 +148,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';
@@ -156,7 +171,7 @@ sub email_sign_in : Private {
$token_data->{twitter_id} = $c->session->{oauth}{twitter_id}
if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id};
if ($c->stash->{current_user}) {
- $token_data->{old_email} = $c->stash->{current_user}->email;
+ $token_data->{old_user_id} = $c->stash->{current_user}->id;
$token_data->{r} = 'auth/change_email/success';
}
@@ -171,6 +186,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
@@ -181,53 +210,43 @@ 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;
- }
-
- if ( $token_obj->created < DateTime->now->subtract( days => 1 ) ) {
- $c->stash->{token_not_found} = 1;
- return;
- }
+ $c->stash->{token_not_found} = 1, return
+ if $data->{old_user_id} && (!$c->user_exists || $c->user->id ne $data->{old_user_id});
- # find or create the user related to the token.
- my $data = $token_obj->data;
+ my $type = $data->{login_type} || 'email';
+ $c->detach( '/auth/process_login', [ $data, $type ] );
+}
- 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} });
+ my $ver = "${type}_verified";
# Bail out if this is a new user and SIGNUPS_DISABLED is set
$c->detach( '/page_error_403_access_denied', [] )
- if FixMyStreet->config('SIGNUPS_DISABLED') && !$user->in_storage && !$data->{old_email};
+ if FixMyStreet->config('SIGNUPS_DISABLED') && !$user->in_storage && !$data->{old_user_id};
- if ($data->{old_email}) {
- # Were logged in as old_email, want to switch to email ($user)
+ if ($data->{old_user_id}) {
+ # Were logged in as old_user_id, want to switch to $user
if ($user->in_storage) {
- my $old_user = $c->model('DB::User')->find({ email => $data->{old_email} });
+ my $old_user = $c->model('DB::User')->find({ id => $data->{old_user_id} });
if ($old_user) {
$old_user->adopt($user);
$user = $old_user;
- $user->email($data->{email});
+ $user->$type($data->{$type});
+ $user->$ver(1);
}
} else {
- # Updating to a new (to the db) email address, easier!
- $user = $c->model('DB::User')->find({ email => $data->{old_email} });
- $user->email($data->{email});
+ # Updating to a new (to the db) email address/phone number, easier!
+ $user = $c->model('DB::User')->find({ id => $data->{old_user_id} });
+ $user->$type($data->{$type});
+ $user->$ver(1);
}
}
@@ -236,193 +255,12 @@ 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 }, 'no_password' );
+ $c->authenticate( { $type => $data->{$type}, $ver => 1 }, 'no_password' );
# send the user to their page
$c->detach( 'redirect_on_signin', [ $data->{r}, $data->{p} ] );
}
-=head2 facebook_sign_in
-
-Starts the Facebook authentication sequence.
-
-=cut
-
-sub fb : Private {
- my ($self, $c) = @_;
- Net::Facebook::Oauth2->new(
- application_id => $c->config->{FACEBOOK_APP_ID},
- application_secret => $c->config->{FACEBOOK_APP_SECRET},
- callback => $c->uri_for('/auth/Facebook'),
- );
-}
-
-sub facebook_sign_in : Private {
- my ( $self, $c ) = @_;
-
- $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
-
- my $fb = $c->forward('/auth/fb');
- my $url = $fb->get_authorization_url(scope => ['email']);
-
- my %oauth;
- $oauth{return_url} = $c->get_param('r');
- $oauth{detach_to} = $c->stash->{detach_to};
- $oauth{detach_args} = $c->stash->{detach_args};
- $c->session->{oauth} = \%oauth;
- $c->res->redirect($url);
-}
-
-=head2 facebook_callback
-
-Handles the Facebook callback request and completes the authentication sequence.
-
-=cut
-
-sub facebook_callback: Path('/auth/Facebook') : Args(0) {
- my ( $self, $c ) = @_;
-
- $c->detach('oauth_failure') if $c->get_param('error_code');
-
- my $fb = $c->forward('/auth/fb');
- my $access_token;
- eval {
- $access_token = $fb->get_access_token(code => $c->get_param('code'));
- };
- if ($@) {
- (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
- $c->detach('/page_error_500_internal_error', [ $message ]);
- }
-
- # save this token in session
- $c->session->{oauth}{token} = $access_token;
-
- my $info = $fb->get('https://graph.facebook.com/me?fields=name,email')->as_hash();
- my $email = lc ($info->{email} || "");
- $c->forward('oauth_success', [ 'facebook', $info->{id}, $info->{name}, $email ]);
-}
-
-=head2 twitter_sign_in
-
-Starts the Twitter authentication sequence.
-
-=cut
-
-sub tw : Private {
- my ($self, $c) = @_;
- Net::Twitter::Lite::WithAPIv1_1->new(
- ssl => 1,
- consumer_key => $c->config->{TWITTER_KEY},
- consumer_secret => $c->config->{TWITTER_SECRET},
- );
-}
-
-sub twitter_sign_in : Private {
- my ( $self, $c ) = @_;
-
- $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
-
- my $twitter = $c->forward('/auth/tw');
- my $url = $twitter->get_authentication_url(callback => $c->uri_for('/auth/Twitter'));
-
- my %oauth;
- $oauth{return_url} = $c->get_param('r');
- $oauth{detach_to} = $c->stash->{detach_to};
- $oauth{detach_args} = $c->stash->{detach_args};
- $oauth{token} = $twitter->request_token;
- $oauth{token_secret} = $twitter->request_token_secret;
- $c->session->{oauth} = \%oauth;
- $c->res->redirect($url);
-}
-
-=head2 twitter_callback
-
-Handles the Twitter callback request and completes the authentication sequence.
-
-=cut
-
-sub twitter_callback: Path('/auth/Twitter') : Args(0) {
- my ( $self, $c ) = @_;
-
- my $request_token = $c->req->param('oauth_token');
- my $verifier = $c->req->param('oauth_verifier');
- my $oauth = $c->session->{oauth};
-
- $c->detach('oauth_failure') if $c->get_param('denied') || $request_token ne $oauth->{token};
-
- my $twitter = $c->forward('/auth/tw');
- $twitter->request_token($oauth->{token});
- $twitter->request_token_secret($oauth->{token_secret});
-
- eval {
- # request_access_token no longer returns UID or name
- $twitter->request_access_token(verifier => $verifier);
- };
- if ($@) {
- (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
- $c->detach('/page_error_500_internal_error', [ $message ]);
- }
-
- my $info = $twitter->verify_credentials();
- $c->forward('oauth_success', [ 'twitter', $info->{id}, $info->{name} ]);
-}
-
-sub oauth_failure : Private {
- my ( $self, $c ) = @_;
-
- $c->stash->{oauth_failure} = 1;
- if ($c->session->{oauth}{detach_to}) {
- $c->detach($c->session->{oauth}{detach_to}, $c->session->{oauth}{detach_args});
- } else {
- $c->stash->{template} = 'auth/general.html';
- $c->detach;
- }
-}
-
-sub oauth_success : Private {
- my ($self, $c, $type, $uid, $name, $email) = @_;
-
- my $user;
- if ($email) {
- # Only Facebook gets here
- # We've got an ID and an email address
- # Remove any existing mention of this ID
- my $existing = $c->model('DB::User')->find( { facebook_id => $uid } );
- $existing->update( { facebook_id => undef } ) if $existing;
- # Get or create a user, give it this Facebook ID
- $user = $c->model('DB::User')->find_or_new( { email => $email } );
- $user->facebook_id($uid);
- $user->name($name);
- $user->in_storage() ? $user->update : $user->insert;
- } else {
- # We've got an ID, but no email
- $user = $c->model('DB::User')->find( { $type . '_id' => $uid } );
- if ($user) {
- # Matching ID in our database
- $user->name($name);
- $user->update;
- } else {
- # No matching ID, store ID for use later
- $c->session->{oauth}{$type . '_id'} = $uid;
- $c->stash->{oauth_need_email} = 1;
- }
- }
-
- # If we've got here with a full user, log in
- if ($user) {
- $c->authenticate( { email => $user->email }, 'no_password' );
- $c->stash->{login_success} = 1;
- }
-
- if ($c->session->{oauth}{detach_to}) {
- $c->detach($c->session->{oauth}{detach_to}, $c->session->{oauth}{detach_args});
- } elsif ($c->stash->{oauth_need_email}) {
- $c->stash->{template} = 'auth/general.html';
- } else {
- $c->detach( 'redirect_on_signin', [ $c->session->{oauth}{return_url} ] );
- }
-}
-
=head2 redirect_on_signin
Used after signing in to take the person back to where they were.
@@ -478,69 +316,6 @@ sub redirect : Private {
}
-=head2 change_password
-
-Let the user change their password.
-
-=cut
-
-sub change_password : Local {
- my ( $self, $c ) = @_;
-
- $c->detach( 'redirect' ) unless $c->user;
-
- $c->forward('get_csrf_token');
-
- # If not a post then no submission
- return unless $c->req->method eq 'POST';
-
- $c->forward('check_csrf_token');
-
- # get the passwords
- my $new = $c->get_param('new_password') // '';
- my $confirm = $c->get_param('confirm') // '';
-
- # check for errors
- my $password_error =
- !$new && !$confirm ? 'missing'
- : $new ne $confirm ? 'mismatch'
- : '';
-
- if ($password_error) {
- $c->stash->{password_error} = $password_error;
- $c->stash->{new_password} = $new;
- $c->stash->{confirm} = $confirm;
- return;
- }
-
- # we should have a usable password - save it to the user
- $c->user->obj->update( { password => $new } );
- $c->stash->{password_changed} = 1;
-
-}
-
-=head2 change_email
-
-Let the user change their email.
-
-=cut
-
-sub change_email : Local {
- my ( $self, $c ) = @_;
-
- $c->detach( 'redirect' ) unless $c->user;
-
- $c->forward('get_csrf_token');
-
- # If not a post then no submission
- return unless $c->req->method eq 'POST';
-
- $c->forward('check_csrf_token');
- $c->stash->{current_user} = $c->user;
- $c->stash->{email_template} = 'change_email.txt';
- $c->forward('email_sign_in');
-}
-
sub get_csrf_token : Private {
my ( $self, $c ) = @_;
@@ -588,7 +363,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..4e9f92596
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm
@@ -0,0 +1,100 @@
+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_error} = 'nonmobile';
+ return;
+ }
+
+ (my $number = $phone->format) =~ s/\s+//g;
+
+ if ( FixMyStreet->config('SIGNUPS_DISABLED')
+ && !$c->model('DB::User')->find({ phone => $number })
+ && !$c->stash->{current_user} # don't break the change phone flow
+ ) {
+ $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,
+ };
+ if ($c->stash->{current_user}) {
+ $token_data->{old_user_id} = $c->stash->{current_user}->id;
+ $token_data->{r} = 'auth/change_phone/success';
+ }
+
+ $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
new file mode 100644
index 000000000..ecf009150
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm
@@ -0,0 +1,151 @@
+package FixMyStreet::App::Controller::Auth::Profile;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Auth::Profile - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Controller for all the authentication profile related pages - adding/ changing/
+verifying email, phone, password.
+
+=head1 METHODS
+
+=cut
+
+sub auto {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/auth/redirect' ) unless $c->user;
+
+ return 1;
+}
+
+=head2 change_password
+
+Let the user change their password.
+
+=cut
+
+sub change_password : Path('/auth/change_password') {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'auth/change_password.html';
+
+ $c->forward('/auth/get_csrf_token');
+
+ # If not a post then no submission
+ return unless $c->req->method eq 'POST';
+
+ $c->forward('/auth/check_csrf_token');
+
+ # get the passwords
+ my $new = $c->get_param('new_password') // '';
+ my $confirm = $c->get_param('confirm') // '';
+
+ # check for errors
+ my $password_error =
+ !$new && !$confirm ? 'missing'
+ : $new ne $confirm ? 'mismatch'
+ : '';
+
+ if ($password_error) {
+ $c->stash->{password_error} = $password_error;
+ $c->stash->{new_password} = $new;
+ $c->stash->{confirm} = $confirm;
+ return;
+ }
+
+ # we should have a usable password - save it to the user
+ $c->user->obj->update( { password => $new } );
+ $c->stash->{password_changed} = 1;
+
+}
+
+=head2 change_email
+
+Let the user change their email.
+
+=cut
+
+sub change_email : Path('/auth/change_email') {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'auth/change_email.html';
+
+ $c->forward('/auth/get_csrf_token');
+
+ # If not a post then no submission
+ return unless $c->req->method eq 'POST';
+
+ $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->get_param('email') ]);
+}
+
+sub change_phone : Path('/auth/change_phone') {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'auth/change_phone.html';
+
+ $c->forward('/auth/get_csrf_token');
+
+ # If not a post then no submission
+ return unless $c->req->method eq 'POST';
+
+ $c->forward('/auth/check_csrf_token');
+ $c->stash->{current_user} = $c->user;
+
+ my $phone = $c->stash->{username} = $c->get_param('username') || '';
+ my $parsed = FixMyStreet::SMS->parse_username($phone);
+
+ # Allow removal of phone number, if we have verified email
+ if (!$phone && !$c->stash->{verifying} && $c->user->email_verified) {
+ $c->user->update({ phone => undef, phone_verified => 0 });
+ $c->flash->{flash_message} = _('You have successfully removed your phone number.');
+ $c->res->redirect('/my');
+ $c->detach;
+ }
+
+ $c->stash->{username_error} = 'missing_phone', return unless $phone;
+ $c->stash->{username_error} = 'other_phone', return unless $parsed->{phone};
+
+ # If we've not used a mobile and we're not specifically verifying,
+ # and phone isn't our only verified way of logging in,
+ # then allow change of number (for e.g. landline).
+ if (!FixMyStreet->config('SMS_AUTHENTICATION') || (!$parsed->{phone}->is_mobile && !$c->stash->{verifying} && $c->user->email_verified)) {
+ $c->user->update({ phone => $phone, phone_verified => 0 });
+ $c->flash->{flash_message} = _('You have successfully added your phone number.');
+ $c->res->redirect('/my');
+ $c->detach;
+ }
+
+ $c->forward('/auth/phone/sign_in', [ $parsed->{phone} ]);
+}
+
+sub verify_item : Path('/auth/verify') : Args(1) {
+ my ( $self, $c, $type ) = @_;
+ $c->stash->{verifying} = 1;
+ $c->detach("change_$type");
+}
+
+sub change_email_success : Path('/auth/change_email/success') {
+ my ( $self, $c ) = @_;
+ $c->flash->{flash_message} = _('You have successfully confirmed your email address.');
+ $c->res->redirect('/my');
+}
+
+sub change_phone_success : Path('/auth/change_phone/success') {
+ my ( $self, $c ) = @_;
+ $c->flash->{flash_message} = _('You have successfully verified your phone number.');
+ $c->res->redirect('/my');
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
new file mode 100644
index 000000000..097cac984
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
@@ -0,0 +1,203 @@
+package FixMyStreet::App::Controller::Auth::Social;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use Net::Facebook::Oauth2;
+use Net::Twitter::Lite::WithAPIv1_1;
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Auth::Social - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Controller for the Facebook/Twitter authentication.
+
+=head1 METHODS
+
+=head2 facebook_sign_in
+
+Starts the Facebook authentication sequence.
+
+=cut
+
+sub fb : Private {
+ my ($self, $c) = @_;
+ Net::Facebook::Oauth2->new(
+ application_id => $c->config->{FACEBOOK_APP_ID},
+ application_secret => $c->config->{FACEBOOK_APP_SECRET},
+ callback => $c->uri_for('/auth/Facebook'),
+ );
+}
+
+sub facebook_sign_in : Private {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
+
+ my $fb = $c->forward('fb');
+ my $url = $fb->get_authorization_url(scope => ['email']);
+
+ my %oauth;
+ $oauth{return_url} = $c->get_param('r');
+ $oauth{detach_to} = $c->stash->{detach_to};
+ $oauth{detach_args} = $c->stash->{detach_args};
+ $c->session->{oauth} = \%oauth;
+ $c->res->redirect($url);
+}
+
+=head2 facebook_callback
+
+Handles the Facebook callback request and completes the authentication sequence.
+
+=cut
+
+sub facebook_callback: Path('/auth/Facebook') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->detach('oauth_failure') if $c->get_param('error_code');
+
+ my $fb = $c->forward('fb');
+ my $access_token;
+ eval {
+ $access_token = $fb->get_access_token(code => $c->get_param('code'));
+ };
+ if ($@) {
+ (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
+ $c->detach('/page_error_500_internal_error', [ $message ]);
+ }
+
+ # save this token in session
+ $c->session->{oauth}{token} = $access_token;
+
+ my $info = $fb->get('https://graph.facebook.com/me?fields=name,email')->as_hash();
+ my $email = lc ($info->{email} || "");
+ $c->forward('oauth_success', [ 'facebook', $info->{id}, $info->{name}, $email ]);
+}
+
+=head2 twitter_sign_in
+
+Starts the Twitter authentication sequence.
+
+=cut
+
+sub tw : Private {
+ my ($self, $c) = @_;
+ Net::Twitter::Lite::WithAPIv1_1->new(
+ ssl => 1,
+ consumer_key => $c->config->{TWITTER_KEY},
+ consumer_secret => $c->config->{TWITTER_SECRET},
+ );
+}
+
+sub twitter_sign_in : Private {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
+
+ my $twitter = $c->forward('tw');
+ my $url = $twitter->get_authentication_url(callback => $c->uri_for('/auth/Twitter'));
+
+ my %oauth;
+ $oauth{return_url} = $c->get_param('r');
+ $oauth{detach_to} = $c->stash->{detach_to};
+ $oauth{detach_args} = $c->stash->{detach_args};
+ $oauth{token} = $twitter->request_token;
+ $oauth{token_secret} = $twitter->request_token_secret;
+ $c->session->{oauth} = \%oauth;
+ $c->res->redirect($url);
+}
+
+=head2 twitter_callback
+
+Handles the Twitter callback request and completes the authentication sequence.
+
+=cut
+
+sub twitter_callback: Path('/auth/Twitter') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $request_token = $c->req->param('oauth_token');
+ my $verifier = $c->req->param('oauth_verifier');
+ my $oauth = $c->session->{oauth};
+
+ $c->detach('oauth_failure') if $c->get_param('denied') || $request_token ne $oauth->{token};
+
+ my $twitter = $c->forward('tw');
+ $twitter->request_token($oauth->{token});
+ $twitter->request_token_secret($oauth->{token_secret});
+
+ eval {
+ # request_access_token no longer returns UID or name
+ $twitter->request_access_token(verifier => $verifier);
+ };
+ if ($@) {
+ (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
+ $c->detach('/page_error_500_internal_error', [ $message ]);
+ }
+
+ my $info = $twitter->verify_credentials();
+ $c->forward('oauth_success', [ 'twitter', $info->{id}, $info->{name} ]);
+}
+
+sub oauth_failure : Private {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{oauth_failure} = 1;
+ if ($c->session->{oauth}{detach_to}) {
+ $c->detach($c->session->{oauth}{detach_to}, $c->session->{oauth}{detach_args});
+ } else {
+ $c->stash->{template} = 'auth/general.html';
+ $c->detach;
+ }
+}
+
+sub oauth_success : Private {
+ my ($self, $c, $type, $uid, $name, $email) = @_;
+
+ my $user;
+ if ($email) {
+ # Only Facebook gets here
+ # We've got an ID and an email address
+ # Remove any existing mention of this ID
+ my $existing = $c->model('DB::User')->find( { facebook_id => $uid } );
+ $existing->update( { facebook_id => undef } ) if $existing;
+ # Get or create a user, give it this Facebook ID
+ $user = $c->model('DB::User')->find_or_new( { email => $email } );
+ $user->facebook_id($uid);
+ $user->name($name);
+ $user->in_storage() ? $user->update : $user->insert;
+ } else {
+ # We've got an ID, but no email
+ $user = $c->model('DB::User')->find( { $type . '_id' => $uid } );
+ if ($user) {
+ # Matching ID in our database
+ $user->name($name);
+ $user->update;
+ } else {
+ # No matching ID, store ID for use later
+ $c->session->{oauth}{$type . '_id'} = $uid;
+ $c->stash->{oauth_need_email} = 1;
+ }
+ }
+
+ # If we've got here with a full user, log in
+ if ($user) {
+ $c->authenticate( { email => $user->email, email_verified => 1 }, 'no_password' );
+ $c->stash->{login_success} = 1;
+ }
+
+ if ($c->session->{oauth}{detach_to}) {
+ $c->detach($c->session->{oauth}{detach_to}, $c->session->{oauth}{detach_args});
+ } elsif ($c->stash->{oauth_need_email}) {
+ $c->stash->{template} = 'auth/general.html';
+ } else {
+ $c->detach( '/auth/redirect_on_signin', [ $c->session->{oauth}{return_url} ] );
+ }
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index e2ab16b6b..1313b5071 100644
--- a/perllib/FixMyStreet/App/Controller/Moderate.pm
+++ b/perllib/FixMyStreet/App/Controller/Moderate.pm
@@ -106,19 +106,21 @@ sub report_moderate_audit : Private {
reason => (sprintf '%s (%s)', $reason, $types_csv),
});
- my $token = $c->model("DB::Token")->create({
- scope => 'moderation',
- data => { id => $problem->id }
- });
-
- $c->send_email( 'problem-moderated.txt', {
- to => [ [ $problem->user->email, $problem->name ] ],
- types => $types_csv,
- user => $problem->user,
- problem => $problem,
- report_uri => $c->stash->{report_uri},
- report_complain_uri => $c->stash->{cobrand_base} . '/contact?m=' . $token->token,
- });
+ if ($problem->user->email_verified) {
+ my $token = $c->model("DB::Token")->create({
+ scope => 'moderation',
+ data => { id => $problem->id }
+ });
+
+ $c->send_email( 'problem-moderated.txt', {
+ to => [ [ $problem->user->email, $problem->name ] ],
+ types => $types_csv,
+ user => $problem->user,
+ problem => $problem,
+ report_uri => $c->stash->{report_uri},
+ report_complain_uri => $c->stash->{cobrand_base} . '/contact?m=' . $token->token,
+ });
+ }
}
sub report_moderate_hide : Private {
diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm
index 5b80a4a08..9647fae9a 100644
--- a/perllib/FixMyStreet/App/Controller/My.pm
+++ b/perllib/FixMyStreet/App/Controller/My.pm
@@ -176,6 +176,10 @@ sub setup_page_data : Private {
any_zoom => 1,
)
if @$pins;
+
+ foreach (qw(flash_message)) {
+ $c->stash->{$_} = $c->flash->{$_} if $c->flash->{$_};
+ }
}
sub planned_change : Path('planned/change') {
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index f92a5cb22..fa3967bf3 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -13,6 +13,7 @@ use Path::Class;
use Utils;
use mySociety::EmailUtil;
use JSON::MaybeXS;
+use FixMyStreet::SMS;
=head1 NAME
@@ -116,19 +117,25 @@ sub report_new : Path : Args(0) {
$c->forward('redirect_or_confirm_creation');
}
-# This is for the new phonegap versions of the app. It looks a lot like
-# report_new but there's a few workflow differences as we only ever want
-# to sent JSON back here
-
sub report_new_test : Path('_test_') : Args(0) {
my ( $self, $c ) = @_;
$c->stash->{template} = 'email_sent.html';
$c->stash->{email_type} = $c->get_param('email_type');
}
+# This is for the new phonegap versions of the app. It looks a lot like
+# report_new but there's a few workflow differences as we only ever want
+# to sent JSON back here
+
sub report_new_ajax : Path('mobile') : Args(0) {
my ( $self, $c ) = @_;
+ # Apps are sending email as username
+ # Prepare for when they upgrade
+ if (!$c->get_param('username')) {
+ $c->set_param('username', $c->get_param('email'));
+ }
+
# create the report - loading a partial if available
$c->forward('initialize_report');
@@ -354,8 +361,12 @@ sub report_import : Path('/import') {
my $report_user = $c->model('DB::User')->find_or_create(
{
email => lc $input{email},
+ email_verified => 1,
name => $input{name},
phone => $input{phone}
+ },
+ {
+ key => 'users_email_verified_key'
}
);
@@ -447,7 +458,7 @@ sub initialize_report : Private {
if ($report) {
# log the problem creation user in to the site
- $c->authenticate( { email => $report->user->email },
+ $c->authenticate( { email => $report->user->email, email_verified => 1 },
'no_password' );
# save the token to delete at the end
@@ -733,14 +744,12 @@ sub process_user : Private {
# Extract all the params to a hash to make them easier to work with
my %params = map { $_ => $c->get_param($_) }
- ( 'email', 'name', 'phone', 'password_register', 'fms_extra_title' );
-
- my $user_title = Utils::trim_text( $params{fms_extra_title} );
+ ( 'username', 'email', 'name', 'phone', 'password_register', 'fms_extra_title' );
if ( $c->cobrand->allow_anonymous_reports ) {
my $anon_details = $c->cobrand->anonymous_account;
- for my $key ( qw( email name ) ) {
+ for my $key ( qw( username email name ) ) {
$params{ $key } ||= $anon_details->{ $key };
}
}
@@ -755,34 +764,29 @@ sub process_user : Private {
last;
}
- $user->name( Utils::trim_text( $params{name} ) ) if $params{name};
- $user->phone( Utils::trim_text( $params{phone} ) );
- $user->title( $user_title ) if $user_title;
$report->user( $user );
+ $c->forward('update_user', [ \%params ]);
if ($c->stash->{contributing_as_body} = $user->contributing_as('body', $c, $c->stash->{bodies}) or
$c->stash->{contributing_as_anonymous_user} = $user->contributing_as('anonymous_user', $c, $c->stash->{bodies})) {
$report->name($user->from_body->name);
$user->name($user->from_body->name) unless $user->name;
$c->stash->{no_reporter_alert} = 1;
- } else {
- $report->name($user->name);
}
return 1;
} }
- # cleanup the email address
- my $email = $params{email} ? lc $params{email} : '';
- $email =~ s{\s+}{}g;
-
- $report->user( $c->model('DB::User')->find_or_new( { email => $email } ) )
+ my $parsed = FixMyStreet::SMS->parse_username($params{username});
+ my $type = $parsed->{type} || 'email';
+ $type = 'email' unless FixMyStreet->config('SMS_AUTHENTICATION');
+ $report->user( $c->model('DB::User')->find_or_new( { $type => $parsed->{username} } ) )
unless $report->user;
- # The user is trying to sign in. We only care about email from the params.
+ # The user is trying to sign in. We only care about username from the params.
if ( $c->get_param('submit_sign_in') || $c->get_param('password_sign_in') ) {
- unless ( $c->forward( '/auth/sign_in' ) ) {
- $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.');
+ unless ( $c->forward( '/auth/sign_in', [ $params{username} ] ) ) {
+ $c->stash->{field_errors}->{password} = _('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.');
return 1;
}
my $user = $c->user->obj;
@@ -794,17 +798,28 @@ sub process_user : Private {
return 1;
}
- # set the user's name, phone, and password
- $report->user->name( Utils::trim_text( $params{name} ) ) if $params{name};
- $report->user->phone( Utils::trim_text( $params{phone} ) );
+ $c->forward('update_user', [ \%params ]);
$report->user->password( Utils::trim_text( $params{password_register} ) )
if $params{password_register};
- $report->user->title( $user_title ) if $user_title;
- $report->name( Utils::trim_text( $params{name} ) );
return 1;
}
+sub update_user : Private {
+ my ($self, $c, $params) = @_;
+ my $report = $c->stash->{report};
+ my $user = $report->user;
+ $user->name( Utils::trim_text( $params->{name} ) );
+ $report->name($user->name);
+ if (!$user->phone_verified) {
+ $user->phone( Utils::trim_text( $params->{phone} ) );
+ } elsif (!$user->email_verified) {
+ $user->email( Utils::trim_text( $params->{email} ) );
+ }
+ my $user_title = Utils::trim_text( $params->{fms_extra_title} );
+ $user->title( $user_title ) if $user_title;
+}
+
=head2 process_report
Looking at the parameters passed in create a new item and return it. Does not
@@ -1027,11 +1042,11 @@ sub check_for_errors : Private {
delete $field_errors{name};
}
- # if using social login then we don't care about name and email errors
+ # if using social login then we don't care about other errors
$c->stash->{is_social_user} = $c->get_param('facebook_sign_in') || $c->get_param('twitter_sign_in');
if ( $c->stash->{is_social_user} ) {
delete $field_errors{name};
- delete $field_errors{email};
+ delete $field_errors{username};
}
# add the photo error if there is one.
@@ -1052,7 +1067,8 @@ sub tokenize_user : Private {
my ($self, $c, $report) = @_;
$c->stash->{token_data} = {
name => $report->user->name,
- phone => $report->user->phone,
+ (!$report->user->phone_verified ? (phone => $report->user->phone) : ()),
+ (!$report->user->email_verified ? (email => $report->user->email) : ()),
password => $report->user->password,
title => $report->user->title,
};
@@ -1085,6 +1101,114 @@ sub send_problem_confirm_email : Private {
} );
}
+sub send_problem_confirm_text : Private {
+ my ( $self, $c ) = @_;
+ my $data = $c->stash->{token_data} || {};
+ my $report = $c->stash->{report};
+
+ $data->{id} = $report->id;
+ $c->forward('/auth/phone/send_token', [ $data, 'problem', $report->user->phone ]);
+ $c->stash->{submit_url} = '/report/new/text';
+}
+
+sub confirm_by_text : Path('text') {
+ my ( $self, $c ) = @_;
+
+ my $token = $c->stash->{token} = $c->get_param('token');
+ my $code = $c->get_param('code') || '';
+
+ my $data = $c->stash->{token_data} = $c->forward('/auth/get_token', [ $token, 'problem' ]) || return;
+ if ($data->{code} ne $code) {
+ $c->stash->{template} = 'auth/smsform.html';
+ $c->stash->{submit_url} = '/report/new/text';
+ $c->stash->{incorrect_code} = 1;
+ return;
+ }
+
+ $c->detach('process_confirmation');
+}
+
+sub process_confirmation : Private {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'tokens/confirm_problem.html';
+ my $data = $c->stash->{token_data};
+
+ unless ($c->stash->{report}) {
+ # Look at all problems, not just cobrand, in case am approving something we don't actually show
+ $c->stash->{report} = $c->model('DB::Problem')->find({ id => $data->{id} }) || return;
+ }
+ my $problem = $c->stash->{report};
+
+ # check that this email or domain are not the cause of abuse. If so hide it.
+ if ( $problem->is_from_abuser ) {
+ $problem->update(
+ { state => 'hidden', lastupdate => \'current_timestamp' } );
+ $c->stash->{template} = 'tokens/abuse.html';
+ return;
+ }
+
+ # For Zurich, email confirmation simply sets a flag, it does not change the
+ # problem state, log in, or anything else
+ if ($c->cobrand->moniker eq 'zurich') {
+ $problem->set_extra_metadata( email_confirmed => 1 );
+ $problem->update( {
+ confirmed => \'current_timestamp',
+ } );
+
+ if ( $data->{name} || $data->{password} ) {
+ $problem->user->name( $data->{name} ) if $data->{name};
+ $problem->user->phone( $data->{phone} ) if $data->{phone};
+ $problem->user->update;
+ }
+
+ return 1;
+ }
+
+ if ($problem->state ne 'unconfirmed') {
+ my $report_uri = $c->cobrand->base_url_for_report( $problem ) . $problem->url;
+ $c->res->redirect($report_uri);
+ return;
+ }
+
+ # We have an unconfirmed problem
+ $problem->update(
+ {
+ state => 'confirmed',
+ confirmed => \'current_timestamp',
+ lastupdate => \'current_timestamp',
+ }
+ );
+
+ # Subscribe problem reporter to email updates
+ $c->forward( '/report/new/create_reporter_alert' );
+
+ # log the problem creation user in to the site
+ if ( $data->{name} || $data->{password} ) {
+ if (!$problem->user->email_verified) {
+ $problem->user->email( $data->{email} ) if $data->{email};
+ } elsif (!$problem->user->phone_verified) {
+ $problem->user->phone( $data->{phone} ) if $data->{phone};
+ }
+ $problem->user->password( $data->{password}, 1 ) if $data->{password};
+ for (qw(name title facebook_id twitter_id)) {
+ $problem->user->$_( $data->{$_} ) if $data->{$_};
+ }
+ $problem->user->update;
+ }
+ if ($problem->user->email_verified) {
+ $c->authenticate( { email => $problem->user->email, email_verified => 1 }, 'no_password' );
+ } elsif ($problem->user->phone_verified) {
+ $c->authenticate( { phone => $problem->user->phone, phone_verified => 1 }, 'no_password' );
+ } else {
+ warn "Reached user authentication with no username verification";
+ }
+ $c->set_session_cookie_expire(0);
+
+ $c->stash->{created_report} = 'fromemail';
+ return 1;
+}
+
=head2 save_user_and_report
Save the user and the report.
@@ -1131,19 +1255,15 @@ sub save_user_and_report : Private {
$c->stash->{detach_args} = [$token->token];
if ( $c->get_param('facebook_sign_in') ) {
- $c->detach('/auth/facebook_sign_in');
+ $c->detach('/auth/social/facebook_sign_in');
} elsif ( $c->get_param('twitter_sign_in') ) {
- $c->detach('/auth/twitter_sign_in');
+ $c->detach('/auth/social/twitter_sign_in');
}
}
# Save or update the user if appropriate
if ( $c->cobrand->never_confirm_reports ) {
- if ( $report->user->in_storage() ) {
- $report->user->update();
- } else {
- $report->user->insert();
- }
+ $report->user->update_or_insert;
$report->confirm();
} elsif ( $c->forward('created_as_someone_else', [ $c->stash->{bodies} ]) ) {
# If created on behalf of someone else, we automatically confirm it,
@@ -1153,7 +1273,11 @@ sub save_user_and_report : Private {
# User does not exist.
$c->forward('tokenize_user', [ $report ]);
$report->user->name( undef );
- $report->user->phone( undef );
+ if (!$report->user->email_verified) {
+ $report->user->email( undef );
+ } elsif (!$report->user->phone_verified) {
+ $report->user->phone( undef );
+ }
$report->user->password( '', 1 );
$report->user->title( undef );
$report->user->insert();
@@ -1173,8 +1297,7 @@ sub save_user_and_report : Private {
$c->log->info($report->user->id . ' exists, but is not logged in for this report');
}
- # save the report;
- $report->in_storage ? $report->update : $report->insert();
+ $report->update_or_insert;
# tidy up
if ( my $token = $c->stash->{partial_token} ) {
@@ -1260,13 +1383,20 @@ sub redirect_or_confirm_creation : Private {
return 1;
}
- # otherwise email a confirm token to them.
- $c->forward( 'send_problem_confirm_email' );
-
- # tell user that they've been sent an email
- $c->stash->{template} = 'email_sent.html';
- $c->stash->{email_type} = 'problem';
- $c->log->info($report->user->id . ' created ' . $report->id . ', email sent, ' . ($c->stash->{token_data}->{password} ? 'password set' : 'password not set'));
+ # otherwise email or text a confirm token to them.
+ my $thing = 'email';
+ if ($report->user->email_verified) {
+ $c->forward( 'send_problem_confirm_email' );
+ # tell user that they've been sent an email
+ $c->stash->{template} = 'email_sent.html';
+ $c->stash->{email_type} = 'problem';
+ } elsif ($report->user->phone_verified) {
+ $c->forward( 'send_problem_confirm_text' );
+ $thing = 'text';
+ } else {
+ warn "Reached problem confirmation with no username verification";
+ }
+ $c->log->info($report->user->id . ' created ' . $report->id . ", $thing sent, " . ($c->stash->{token_data}->{password} ? 'password set' : 'password not set'));
}
sub create_reporter_alert : Private {
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index 033f5c017..66724f2d1 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -36,18 +36,6 @@ sub report_update : Path : Args(0) {
$c->forward('redirect_or_confirm_creation');
}
-sub confirm : Private {
- my ( $self, $c ) = @_;
-
- $c->stash->{update}->confirm;
- $c->stash->{update}->update;
-
- $c->forward('update_problem');
- $c->forward('signup_for_alerts');
-
- return 1;
-}
-
sub update_problem : Private {
my ( $self, $c ) = @_;
@@ -109,6 +97,10 @@ sub process_user : Private {
my $update = $c->stash->{update};
+ # Extract all the params to a hash to make them easier to work with
+ my %params = map { $_ => $c->get_param($_) }
+ ( 'username', 'name', 'password_register', 'fms_extra_title' );
+
# Extra block to use 'last'
if ( $c->user_exists ) { {
my $user = $c->user->obj;
@@ -118,13 +110,9 @@ sub process_user : Private {
last;
}
- my $name = $c->get_param('name');
- $user->name( Utils::trim_text( $name ) ) if $name;
- my $title = $c->get_param('fms_extra_title');
- if ( $title ) {
- $c->log->debug( 'user exists and title is ' . $title );
- $user->title( Utils::trim_text( $title ) );
- }
+ $user->name( Utils::trim_text( $params{name} ) ) if $params{name};
+ my $title = Utils::trim_text( $params{fms_extra_title} );
+ $user->title( $title ) if $title;
$update->user( $user );
# Just in case, make sure the user will have a name
@@ -135,21 +123,16 @@ sub process_user : Private {
return 1;
} }
- # Extract all the params to a hash to make them easier to work with
- my %params = map { $_ => $c->get_param($_) }
- ( 'rznvy', 'name', 'password_register', 'fms_extra_title' );
-
- # cleanup the email address
- my $email = $params{rznvy} ? lc $params{rznvy} : '';
- $email =~ s{\s+}{}g;
-
- $update->user( $c->model('DB::User')->find_or_new( { email => $email } ) )
+ my $parsed = FixMyStreet::SMS->parse_username($params{username});
+ my $type = $parsed->{type} || 'email';
+ $type = 'email' unless FixMyStreet->config('SMS_AUTHENTICATION');
+ $update->user( $c->model('DB::User')->find_or_new( { $type => $parsed->{username} } ) )
unless $update->user;
- # The user is trying to sign in. We only care about email from the params.
+ # The user is trying to sign in. We only care about username from the params.
if ( $c->get_param('submit_sign_in') || $c->get_param('password_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.');
+ unless ( $c->forward( '/auth/sign_in', [ $params{username} ] ) ) {
+ $c->stash->{field_errors}->{password} = _('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.');
return 1;
}
my $user = $c->user->obj;
@@ -328,8 +311,6 @@ sub process_update : Private {
$update->extra( $extra );
}
- $c->log->debug( 'name is ' . $c->get_param('name') );
-
$c->stash->{add_alert} = $c->get_param('add_alert');
return 1;
@@ -372,7 +353,7 @@ sub check_for_errors : Private {
$c->stash->{is_social_user} = $c->get_param('facebook_sign_in') || $c->get_param('twitter_sign_in');
if ( $c->stash->{is_social_user} ) {
delete $field_errors{name};
- delete $field_errors{email};
+ delete $field_errors{username};
}
if ( my $photo_error = delete $c->stash->{photo_error} ) {
@@ -438,18 +419,14 @@ sub save_update : Private {
$c->stash->{detach_args} = [$token->token];
if ( $c->get_param('facebook_sign_in') ) {
- $c->detach('/auth/facebook_sign_in');
+ $c->detach('/auth/social/facebook_sign_in');
} elsif ( $c->get_param('twitter_sign_in') ) {
- $c->detach('/auth/twitter_sign_in');
+ $c->detach('/auth/social/twitter_sign_in');
}
}
if ( $c->cobrand->never_confirm_updates ) {
- if ( $update->user->in_storage() ) {
- $update->user->update();
- } else {
- $update->user->insert();
- }
+ $update->user->update_or_insert;
$update->confirm();
} elsif ( $c->forward('/report/new/created_as_someone_else', [ $update->problem->bodies_str ]) ) {
# If created on behalf of someone else, we automatically confirm it,
@@ -464,7 +441,6 @@ sub save_update : Private {
}
elsif ( $c->user && $c->user->id == $update->user->id ) {
# Logged in and same user, so can confirm update straight away
- $c->log->debug( 'user exists' );
$update->user->update;
$update->confirm;
} else {
@@ -473,12 +449,7 @@ sub save_update : Private {
$update->user->discard_changes();
}
- if ( $update->in_storage ) {
- $update->update;
- }
- else {
- $update->insert;
- }
+ $update->update_or_insert;
return 1;
}
@@ -507,28 +478,108 @@ sub redirect_or_confirm_creation : Private {
return 1;
}
- # otherwise create a confirm token and email it to them.
- my $data = $c->stash->{token_data} || {};
- my $token = $c->model("DB::Token")->create(
- {
- scope => 'comment',
- data => {
- %$data,
- id => $update->id,
- add_alert => ( $c->get_param('add_alert') ? 1 : 0 ),
- }
- }
- );
+ my $data = $c->stash->{token_data};
+ $data->{id} = $update->id;
+ $data->{add_alert} = $c->get_param('add_alert') ? 1 : 0;
+
+ if ($update->user->email_verified) {
+ $c->forward('send_confirmation_email');
+ # tell user that they've been sent an email
+ $c->stash->{template} = 'email_sent.html';
+ $c->stash->{email_type} = 'update';
+ } elsif ($update->user->phone_verified) {
+ $c->forward('send_confirmation_text');
+ } else {
+ warn "Reached update confirmation with no username verification";
+ }
+
+ return 1;
+}
+
+sub send_confirmation_email : Private {
+ my ( $self, $c ) = @_;
+
+ my $update = $c->stash->{update};
+ my $token = $c->model("DB::Token")->create( {
+ scope => 'comment',
+ data => $c->stash->{token_data},
+ } );
+ my $template = 'update-confirm.txt';
$c->stash->{token_url} = $c->uri_for_email( '/C', $token->token );
- $c->send_email( 'update-confirm.txt', {
- to => $update->name
- ? [ [ $update->user->email, $update->name ] ]
- : $update->user->email,
+ $c->send_email( $template, {
+ to => [ $update->name ? [ $update->user->email, $update->name ] : $update->user->email ],
} );
+}
+
+sub send_confirmation_text : Private {
+ my ( $self, $c ) = @_;
+ my $update = $c->stash->{update};
+ $c->forward('/auth/phone/send_token', [ $c->stash->{token_data}, 'comment', $update->user->phone ]);
+ $c->stash->{submit_url} = '/report/update/text';
+}
+
+sub confirm_by_text : Path('text') {
+ my ( $self, $c ) = @_;
+
+ my $token = $c->stash->{token} = $c->get_param('token');
+ my $code = $c->get_param('code') || '';
+
+ my $data = $c->stash->{token_data} = $c->forward('/auth/get_token', [ $token, 'comment' ]) || return;
+ if ($data->{code} ne $code) {
+ $c->stash->{template} = 'auth/smsform.html';
+ $c->stash->{submit_url} = '/report/update/text';
+ $c->stash->{incorrect_code} = 1;
+ return;
+ }
+
+ $c->detach('process_confirmation');
+}
+
+sub process_confirmation : Private {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{template} = 'tokens/confirm_update.html';
+ my $data = $c->stash->{token_data};
- # tell user that they've been sent an email
- $c->stash->{template} = 'email_sent.html';
- $c->stash->{email_type} = 'update';
+ unless ($c->stash->{update}) {
+ $c->stash->{update} = $c->model('DB::Comment')->find({ id => $data->{id} }) || return;
+ }
+ my $comment = $c->stash->{update};
+
+ # check that this email or domain are not the cause of abuse. If so hide it.
+ if ( $comment->is_from_abuser ) {
+ $c->stash->{template} = 'tokens/abuse.html';
+ return;
+ }
+
+ if ( $comment->state ne 'unconfirmed' ) {
+ my $report_uri = $c->cobrand->base_url_for_report( $comment->problem ) . $comment->problem->url;
+ $c->res->redirect($report_uri);
+ return;
+ }
+
+ if ( $data->{name} || $data->{password} ) {
+ for (qw(name facebook_id twitter_id)) {
+ $comment->user->$_( $data->{$_} ) if $data->{$_};
+ }
+ $comment->user->password( $data->{password}, 1 ) if $data->{password};
+ $comment->user->update;
+ }
+
+ if ($comment->user->email_verified) {
+ $c->authenticate( { email => $comment->user->email, email_verified => 1 }, 'no_password' );
+ } elsif ($comment->user->phone_verified) {
+ $c->authenticate( { phone => $comment->user->phone, phone_verified => 1 }, 'no_password' );
+ } else {
+ warn "Reached user authentication with no username verification";
+ }
+ $c->set_session_cookie_expire(0);
+
+ $c->stash->{update}->confirm;
+ $c->stash->{update}->update;
+ $c->forward('update_problem');
+ $c->stash->{add_alert} = $data->{add_alert};
+ $c->forward('signup_for_alerts');
return 1;
}
diff --git a/perllib/FixMyStreet/App/Controller/Tokens.pm b/perllib/FixMyStreet/App/Controller/Tokens.pm
index a1b0c57ba..bb6140e0a 100644
--- a/perllib/FixMyStreet/App/Controller/Tokens.pm
+++ b/perllib/FixMyStreet/App/Controller/Tokens.pm
@@ -45,10 +45,10 @@ sub confirm_problem : Path('/P') {
# Load the problem
my $data = $auth_token->data;
$data = { id => $data } unless ref $data;
+ $c->stash->{token_data} = $data;
- my $problem_id = $data->{id};
# Look at all problems, not just cobrand, in case am approving something we don't actually show
- my $problem = $c->model('DB::Problem')->find( { id => $problem_id } )
+ my $problem = $c->model('DB::Problem')->find( { id => $data->{id} } )
|| $c->detach('token_error');
$c->stash->{report} = $problem;
@@ -56,64 +56,7 @@ sub confirm_problem : Path('/P') {
if $problem->state eq 'unconfirmed'
&& $auth_token->created < DateTime->now->subtract( months => 1 );
- # check that this email or domain are not the cause of abuse. If so hide it.
- if ( $problem->is_from_abuser ) {
- $problem->update(
- { state => 'hidden', lastupdate => \'current_timestamp' } );
- $c->stash->{template} = 'tokens/abuse.html';
- return;
- }
-
- # For Zurich, email confirmation simply sets a flag, it does not change the
- # problem state, log in, or anything else
- if ($c->cobrand->moniker eq 'zurich') {
- $problem->set_extra_metadata( email_confirmed => 1 );
- $problem->update( {
- confirmed => \'current_timestamp',
- } );
-
- if ( $data->{name} || $data->{password} ) {
- $problem->user->name( $data->{name} ) if $data->{name};
- $problem->user->phone( $data->{phone} ) if $data->{phone};
- $problem->user->update;
- }
-
- return 1;
- }
-
- if ($problem->state ne 'unconfirmed') {
- my $report_uri = $c->cobrand->base_url_for_report( $problem ) . $problem->url;
- $c->res->redirect($report_uri);
- return;
- }
-
- # We have an unconfirmed problem
- $problem->update(
- {
- state => 'confirmed',
- confirmed => \'current_timestamp',
- lastupdate => \'current_timestamp',
- }
- );
-
- # Subscribe problem reporter to email updates
- $c->forward( '/report/new/create_reporter_alert' );
-
- # log the problem creation user in to the site
- if ( $data->{name} || $data->{password} ) {
- $problem->user->name( $data->{name} ) if $data->{name};
- $problem->user->phone( $data->{phone} ) if $data->{phone};
- $problem->user->password( $data->{password}, 1 ) if $data->{password};
- $problem->user->title( $data->{title} ) if $data->{title};
- $problem->user->facebook_id( $data->{facebook_id} ) if $data->{facebook_id};
- $problem->user->twitter_id( $data->{twitter_id} ) if $data->{twitter_id};
- $problem->user->update;
- }
- $c->authenticate( { email => $problem->user->email }, 'no_password' );
- $c->set_session_cookie_expire(0);
-
- $c->stash->{created_report} = 'fromemail';
- return 1;
+ $c->forward('/report/new/process_confirmation');
}
=head2 redirect_to_partial_problem
@@ -170,7 +113,7 @@ sub confirm_alert : Path('/A') {
}
if (!$alert->confirmed && $c->stash->{confirm_type} ne 'unsubscribe') {
- $c->authenticate( { email => $alert->user->email }, 'no_password' );
+ $c->authenticate( { email => $alert->user->email, email_verified => 1 }, 'no_password' );
$c->set_session_cookie_expire(0);
}
@@ -205,11 +148,9 @@ sub confirm_update : Path('/C') {
$c->forward( 'load_auth_token', [ $token_code, 'comment' ] );
# Load the update
- my $data = $auth_token->data;
- my $comment_id = $data->{id};
- $c->stash->{add_alert} = $data->{add_alert};
+ my $data = $c->stash->{token_data} = $auth_token->data;
- my $comment = $c->model('DB::Comment')->find( { id => $comment_id } )
+ my $comment = $c->model('DB::Comment')->find( { id => $data->{id} } )
|| $c->detach('token_error');
$c->stash->{update} = $comment;
@@ -217,32 +158,7 @@ sub confirm_update : Path('/C') {
if $comment->state ne 'confirmed'
&& $auth_token->created < DateTime->now->subtract( months => 1 );
- # check that this email or domain are not the cause of abuse. If so hide it.
- if ( $comment->is_from_abuser ) {
- $c->stash->{template} = 'tokens/abuse.html';
- return;
- }
-
- if ( $comment->state ne 'unconfirmed' ) {
- my $report_uri = $c->cobrand->base_url_for_report( $comment->problem ) . $comment->problem->url;
- $c->res->redirect($report_uri);
- return;
- }
-
- if ( $data->{name} || $data->{password} ) {
- $comment->user->name( $data->{name} ) if $data->{name};
- $comment->user->password( $data->{password}, 1 ) if $data->{password};
- $comment->user->facebook_id( $data->{facebook_id} ) if $data->{facebook_id};
- $comment->user->twitter_id( $data->{twitter_id} ) if $data->{twitter_id};
- $comment->user->update;
- }
-
- $c->authenticate( { email => $comment->user->email }, 'no_password' );
- $c->set_session_cookie_expire(0);
-
- $c->forward('/report/update/confirm');
-
- return 1;
+ $c->forward('/report/update/process_confirmation');
}
sub load_questionnaire : Private {
@@ -269,7 +185,7 @@ sub questionnaire : Path('/Q') : Args(1) {
my $questionnaire = $c->stash->{questionnaire};
if (!$questionnaire->whenanswered) {
- $c->authenticate( { email => $questionnaire->problem->user->email }, 'no_password' );
+ $c->authenticate( { email => $questionnaire->problem->user->email, email_verified => 1 }, 'no_password' );
$c->set_session_cookie_expire(0);
}
$c->forward( '/questionnaire/show' );