diff options
Diffstat (limited to 'perllib/FixMyStreet/App/Controller')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin.pm | 332 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth.pm | 385 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth/Phone.pm | 100 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth/Profile.pm | 151 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Auth/Social.pm | 203 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Moderate.pm | 28 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/My.pm | 4 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/New.pm | 224 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Report/Update.pm | 185 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Tokens.pm | 100 |
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 ‘sign in by email’ 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 ‘No’ 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 ‘sign in by email’ 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 ‘No’ 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' ); |