Commit 867a5d4a authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

allow users to change their mail address

Closes #6
parent 4ad1a1d2
Loading
Loading
Loading
Loading
+55 −1
Original line number Diff line number Diff line
@@ -800,6 +800,57 @@ sub startup {
		}
	);

	$self->helper(
		'mark_for_mail_change' => sub {
			my ( $self, $db, $uid, $email, $token ) = @_;

			$db->insert(
				'pending_mails',
				{
					user_id => $uid,
					email   => $email,
					token   => $token,
					requested_at =>
					  DateTime->now( time_zone => 'Europe/Berlin' )
				},
				{
					on_conflict => \
'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, requested_at = EXCLUDED.requested_at'
				},
			);
		}
	);

	$self->helper(
		'change_mail_with_token' => sub {
			my ( $self, $uid, $token ) = @_;

			my $db = $self->pg->db;
			my $tx = $db->begin;

			my $res_h = $db->select(
				'pending_mails',
				['email'],
				{
					user_id => $uid,
					token   => $token
				}
			)->hash;

			if ($res_h) {
				$db->update(
					'users',
					{ email => $res_h->{email} },
					{ id    => $uid }
				);
				$db->delete( 'pending_mails', { user_id => $uid } );
				$tx->commit;
				return 1;
			}
			return;
		}
	);

	$self->helper(
		'remove_password_token' => sub {
			my ( $self, $uid, $token ) = @_;
@@ -1004,7 +1055,7 @@ sub startup {
			}

			$count = $self->pg->db->select(
				'pending_mails',
				'mail_blacklist',
				'count(*) as count',
				{
					email     => $mail,
@@ -1638,6 +1689,7 @@ sub startup {
	$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
	$authed_r->get('/cancelled')->to('traveling#cancelled');
	$authed_r->get('/change_password')->to('account#password_form');
	$authed_r->get('/change_mail')->to('account#change_mail');
	$authed_r->get('/export.json')->to('account#json_export');
	$authed_r->get('/history.json')->to('traveling#json_history');
	$authed_r->get('/history')->to('traveling#history');
@@ -1646,9 +1698,11 @@ sub startup {
	$authed_r->get('/journey/add')->to('traveling#add_journey_form');
	$authed_r->get('/journey/:id')->to('traveling#journey_details');
	$authed_r->get('/s/*station')->to('traveling#station');
	$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
	$authed_r->post('/journey/add')->to('traveling#add_journey_form');
	$authed_r->post('/journey/edit')->to('traveling#edit_journey');
	$authed_r->post('/change_password')->to('account#change_password');
	$authed_r->post('/change_mail')->to('account#change_mail');
	$authed_r->post('/delete')->to('account#delete');
	$authed_r->post('/logout')->to('account#do_logout');
	$authed_r->post('/set_token')->to('api#set_token');
+20 −1
Original line number Diff line number Diff line
@@ -378,7 +378,7 @@ my @migrations = (
	},

	# v6 -> v7
	# Add password_reset table to store data about pending password resets
	# Add pending_passwords table to store data about pending password resets
	sub {
		my ($db) = @_;
		$db->query(
@@ -393,6 +393,25 @@ my @migrations = (
			}
		);
	},

	# v7 -> v8
	# Add pending_mails table to store data about pending mail changes
	sub {
		my ($db) = @_;
		$db->query(
			qq{
				alter table pending_mails rename to mail_blacklist;
				create table pending_mails (
					user_id integer not null references users (id) primary key,
					email varchar(256) not null,
					token varchar(80) not null,
					requested_at timestamptz not null
				);
				comment on table pending_mails is 'Verification tokens for mail address changes';
				update schema_version set version = 8;
			}
		);
	},
);

sub setup_db {
+10 −3
Original line number Diff line number Diff line
@@ -34,13 +34,13 @@ sub run {
		);

		my $pending
		  = $db->select( 'pending_mails', ['num_tries'], { email => $mail } );
		  = $db->select( 'mail_blacklist', ['num_tries'], { email => $mail } );
		my $pending_h = $pending->hash;

		if ($pending_h) {
			my $num_tries = $pending_h->{num_tries} + 1;
			$db->update(
				'pending_mails',
				'mail_blacklist',
				{
					num_tries => $num_tries,
					last_try  => $reg_date
@@ -50,7 +50,7 @@ sub run {
		}
		else {
			$db->insert(
				'pending_mails',
				'mail_blacklist',
				{
					email     => $mail,
					num_tries => 1,
@@ -69,6 +69,13 @@ sub run {
		printf( "Pruned %d pending password reset(s)\n", $rows );
	}

	$res = $db->delete( 'pending_mails',
		{ requested_at => { '<', $verification_deadline } } );

	if ( my $rows = $res->rows ) {
		printf( "Pruned %d pending mail change(s)\n", $rows );
	}

	my $to_delete = $db->select( 'users', ['id'],
		{ deletion_requested => { '<', $deletion_deadline } } );
	my @uids_to_delete = $to_delete->arrays->map( sub { shift->[0] } )->each;
+99 −1
Original line number Diff line number Diff line
@@ -211,6 +211,88 @@ sub do_logout {
	$self->redirect_to('/login');
}

sub change_mail {
	my ($self) = @_;

	my $action   = $self->req->param('action');
	my $password = $self->req->param('password');
	my $email    = $self->req->param('email');

	if ( $action and $action eq 'update_mail' ) {
		if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
			$self->render(
				'change_mail',
				invalid => 'csrf',
			);
			return;
		}

		if ( not length($email) ) {
			$self->render( 'change_mail', invalid => 'mail_empty' );
			return;
		}

		if (
			not $self->authenticate(
				$self->current_user->{name},
				$self->param('password')
			)
		  )
		{
			$self->render( 'change_mail', invalid => 'password' );
			return;
		}

		my $token = make_token();
		my $name  = $self->current_user->{name};
		my $db    = $self->pg->db;
		my $tx    = $db->begin;

		$self->mark_for_mail_change( $db, $self->current_user->{id},
			$email, $token );

		my $ip   = $self->req->headers->header('X-Forwarded-For');
		my $ua   = $self->req->headers->user_agent;
		my $date = DateTime->now( time_zone => 'Europe/Berlin' )
		  ->strftime('%d.%m.%Y %H:%M:%S %z');

		# In case Mojolicious is not running behind a reverse proxy
		$ip
		  //= sprintf( '%s:%s', $self->tx->remote_address,
			$self->tx->remote_port );
		my $confirm_url
		  = $self->url_for('confirm_mail')->to_abs->scheme('https');
		my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');

		my $body = "Hallo ${name},\n\n";
		$body .= "Bitte bestätige unter <${confirm_url}/${token}>,\n";
		$body .= "dass du mit dieser Adresse E-Mail empfangen kannst.\n\n";
		$body
		  .= "Du erhältst diese Mail, da eine Änderung der deinem travelynx-Account\n";
		$body .= "zugeordneten Mail-Adresse beantragt wurde.\n\n";
		$body .= "Daten zur Anfrage:\n";
		$body .= " * Datum: ${date}\n";
		$body .= " * Client: ${ip}\n";
		$body .= " * UserAgent: ${ua}\n\n\n";
		$body .= "Impressum: ${imprint_url}\n";

		my $success
		  = $self->sendmail->custom( $email,
			'travelynx: Mail-Adresse bestätigen', $body );

		if ($success) {
			$tx->commit;
			$self->render( 'change_mail', success => 1 );
		}
		else {
			$self->render( 'change_mail', invalid => 'sendmail' );
		}
	}
	else {
		$self->render('change_mail');
	}
}

sub password_form {
	my ($self) = @_;

@@ -252,6 +334,7 @@ sub change_password {
	my $pw_hash = hash_password($password);
	$self->set_user_password( $self->current_user->{id}, $pw_hash );

	$self->flash( success => 'password' );
	$self->redirect_to('account');

	my $user  = $self->current_user->{name};
@@ -361,7 +444,7 @@ sub request_password_reset {
			return;
		}
		if ( not $self->verify_password_token( $id, $token ) ) {
			$self->render( 'recover_password', invalid => 'recovery token' );
			$self->render( 'recover_password', invalid => 'change token' );
			return;
		}
		if ( $password ne $password2 ) {
@@ -384,6 +467,7 @@ sub request_password_reset {
				invalid => 'Authentication failure – WTF?' );
		}

		$self->flash( success => 'password' );
		$self->redirect_to('account');

		$self->remove_password_token( $id, $token );
@@ -433,6 +517,20 @@ sub recover_password {
	}
}

sub confirm_mail {
	my ($self) = @_;
	my $id     = $self->current_user->{id};
	my $token  = $self->stash('token');

	if ( $self->change_mail_with_token( $id, $token ) ) {
		$self->flash( success => 'mail' );
		$self->redirect_to('account');
	}
	else {
		$self->render( 'change_mail', invalid => 'change token' );
	}
}

sub account {
	my ($self) = @_;

+2 −2
Original line number Diff line number Diff line
@@ -69,9 +69,9 @@
					<span class="card-title">Passwort-Reset wird durchgeführt</span>
					<p>Es wurde bereits ein Reset-Link verschickt.</p>
				% }
				% elsif ($invalid eq 'recovery token') {
				% elsif ($invalid eq 'change token') {
					<span class="card-title">Ungültiger Token</span>
					<p>Der Reset-Token ist ungültig oder abgelaufen. Neuen beantragen?</p>
					<p>Der Token ist ungültig oder abgelaufen. Neuen beantragen?</p>
				% }
				% elsif ($invalid eq 'deletion password') {
					<span class="card-title">Ungültiges Passwort</span>
Loading