diff options
Diffstat (limited to 'lib')
| -rwxr-xr-x | lib/Travelynx.pm | 85 | ||||
| -rw-r--r-- | lib/Travelynx/Command/database.pm | 17 | ||||
| -rw-r--r-- | lib/Travelynx/Command/maintenance.pm | 7 | ||||
| -rw-r--r-- | lib/Travelynx/Controller/Account.pm | 154 | 
4 files changed, 263 insertions, 0 deletions
| diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 1e7c965..01515e9 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -732,6 +732,88 @@ sub startup {  		}  	); +	$self->helper( +		'get_uid_by_name_and_mail' => sub { +			my ( $self, $name, $email ) = @_; + +			my $res = $self->pg->db->select( +				'users', +				['id'], +				{ +					name   => $name, +					email  => $email, +					status => 1 +				} +			); + +			if ( my $user = $res->hash ) { +				return $user->{id}; +			} +			return; +		} +	); + +	$self->helper( +		'mark_for_password_reset' => sub { +			my ( $self, $db, $uid, $token ) = @_; + +			my $res = $db->select( +				'pending_passwords', +				'count(*) as count', +				{ user_id => $uid } +			); +			if ( $res->hash->{count} ) { +				return 'in progress'; +			} + +			$db->insert( +				'pending_passwords', +				{ +					user_id => $uid, +					token   => $token, +					requested_at => +					  DateTime->now( time_zone => 'Europe/Berlin' ) +				} +			); + +			return undef; +		} +	); + +	$self->helper( +		'verify_password_token' => sub { +			my ( $self, $uid, $token ) = @_; + +			my $res = $self->pg->db->select( +				'pending_passwords', +				'count(*) as count', +				{ +					user_id => $uid, +					token   => $token +				} +			); + +			if ( $res->hash->{count} ) { +				return 1; +			} +			return; +		} +	); + +	$self->helper( +		'remove_password_token' => sub { +			my ( $self, $uid, $token ) = @_; + +			$self->pg->db->delete( +				'pending_passwords', +				{ +					user_id => $uid, +					token   => $token +				} +			); +		} +	); +  	# This helper should only be called directly when also providing a user ID.  	# If you don't have one, use current_user() instead (get_user_data will  	# delegate to it anyways). @@ -1530,6 +1612,8 @@ sub startup {  	$r->get('/api/v0/:user_action/:token')->to('api#get_v0');  	$r->get('/api/v1/:user_action/:token')->to('api#get_v1');  	$r->get('/login')->to('account#login_form'); +	$r->get('/recover')->to('account#request_password_reset'); +	$r->get('/recover/:id/:token')->to('account#recover_password');  	$r->get('/register')->to('account#registration_form');  	$r->get('/reg/:id/:token')->to('account#verify');  	$r->post('/action')->to('traveling#log_action'); @@ -1537,6 +1621,7 @@ sub startup {  	$r->post('/list_departures')->to('traveling#redirect_to_station');  	$r->post('/login')->to('account#do_login');  	$r->post('/register')->to('account#register'); +	$r->post('/recover')->to('account#request_password_reset');  	my $authed_r = $r->under(  		sub { diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 393564b..62a470c 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -376,6 +376,23 @@ my @migrations = (  			}  		);  	}, + +	# v6 -> v7 +	# Add password_reset table to store data about pending password resets +	sub { +		my ($db) = @_; +		$db->query( +			qq{ +				create table pending_passwords ( +					user_id integer not null references users (id) primary key, +					token varchar(80) not null, +					requested_at timestamptz not null +				); +				comment on table pending_passwords is 'Password reset tokens'; +				update schema_version set version = 7; +			} +		); +	},  );  sub setup_db { diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm index 3b2462c..b3702b4 100644 --- a/lib/Travelynx/Command/maintenance.pm +++ b/lib/Travelynx/Command/maintenance.pm @@ -62,6 +62,13 @@ sub run {  		printf( "Pruned unverified user %d\n", $user->{id} );  	} +	my $res = $db->delete( 'pending_passwords', +		{ requested_at => { '<', $verification_deadline } } ); + +	if ( my $rows = $res->rows ) { +		printf( "Pruned %d pending password reset(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; diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 7249ab8..5f06f2d 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -278,6 +278,160 @@ sub change_password {  	$self->sendmail->custom( $email, 'travelynx: Passwort geändert', $body );  } +sub request_password_reset { +	my ($self) = @_; + +	if ( $self->param('action') and $self->param('action') eq 'initiate' ) { +		if ( $self->validation->csrf_protect->has_error('csrf_token') ) { +			$self->render( 'recover_password', invalid => 'csrf' ); +			return; +		} + +		my $name  = $self->param('user'); +		my $email = $self->param('email'); + +		my $uid = $self->get_uid_by_name_and_mail( $name, $email ); + +		if ( not $uid ) { +			$self->render( 'recover_password', invalid => 'credentials' ); +			return; +		} + +		my $token = make_token(); +		my $db    = $self->pg->db; +		my $tx    = $db->begin; + +		my $error = $self->mark_for_password_reset( $db, $uid, $token ); + +		if ($error) { +			$self->render( 'recover_password', invalid => $error ); +			return; +		} + +		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 $recover_url = $self->url_for('recover')->to_abs->scheme('https'); +		my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https'); + +		my $body = "Hallo ${name},\n\n"; +		$body .= "Unter ${recover_url}/${uid}/${token}\n"; +		$body +		  .= "kannst du ein neues Passwort für deinen travelynx-Account vergeben.\n\n"; +		$body +		  .= "Du erhältst diese Mail, da mit deinem Accountnamen und deiner Mail-Adresse\n"; +		$body +		  .= "ein Passwort-Reset angefordert wurde. Falls diese Anfrage nicht von dir\n"; +		$body .= "ausging, kannst du sie ignorieren.\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: Neues Passwort', +			$body ); + +		if ($success) { +			$tx->commit; +			$self->render( 'recover_password', success => 1 ); +		} +		else { +			$self->render( 'recover_password', invalid => 'sendmail' ); +		} +	} +	elsif ( $self->param('action') +		and $self->param('action') eq 'set_password' ) +	{ +		my $id        = $self->param('id'); +		my $token     = $self->param('token'); +		my $password  = $self->param('newpw'); +		my $password2 = $self->param('newpw2'); + +		if ( $self->validation->csrf_protect->has_error('csrf_token') ) { +			$self->render( 'set_password', invalid => 'csrf' ); +			return; +		} +		if ( not $self->verify_password_token( $id, $token ) ) { +			$self->render( 'recover_password', invalid => 'token' ); +			return; +		} +		if ( $password ne $password2 ) { +			$self->render( 'set_password', invalid => 'password_notequal' ); +			return; +		} + +		if ( length($password) < 8 ) { +			$self->render( 'set_password', invalid => 'password_short' ); +			return; +		} + +		my $pw_hash = hash_password($password); +		$self->set_user_password( $id, $pw_hash ); + +		my $account = $self->get_user_data($id); + +		if ( not $self->authenticate( $account->{name}, $password ) ) { +			$self->render( 'set_password', +				invalid => 'Authentication failure – WTF?' ); +		} + +		$self->redirect_to('account'); + +		$self->remove_password_token( $id, $token ); + +		my $user  = $account->{name}; +		my $email = $account->{email}; +		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 $imprint_url = $self->url_for('impressum')->to_abs->scheme('https'); + +		my $body = "Hallo ${user},\n\n"; +		$body +		  .= "Das Passwort deines travelynx-Accounts wurde soeben über die"; +		$body .= " 'Passwort vergessen'-Funktion geändert.\n\n"; +		$body .= "Daten zur Änderung:\n"; +		$body .= " * Datum: ${date}\n"; +		$body .= " * Client: ${ip}\n"; +		$body .= " * UserAgent: ${ua}\n\n\n"; +		$body .= "Impressum: ${imprint_url}\n"; + +		$self->sendmail->custom( $email, 'travelynx: Passwort geändert', +			$body ); +	} +	else { +		$self->render('recover_password'); +	} +} + +sub recover_password { +	my ($self) = @_; + +	my $id    = $self->stash('id'); +	my $token = $self->stash('token'); + +	if ( $self->verify_password_token( $id, $token ) ) { +		$self->render('set_password'); +	} +	else { +		$self->render( 'recover_password', invalid => 'token' ); +	} +} +  sub account {  	my ($self) = @_; | 
