diff options
Diffstat (limited to 'lib/Travelynx')
| -rw-r--r-- | lib/Travelynx/Controller/Account.pm | 112 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 8 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Users.pm | 434 | 
3 files changed, 522 insertions, 32 deletions
| diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 135c52b..10200a6 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -7,7 +7,7 @@ use UUID::Tiny qw(:std);  sub hash_password {  	my ($password) = @_;  	my @salt_bytes = map { int( rand(255) ) + 1 } ( 1 .. 16 ); -	my $salt = en_base64( pack( 'C[16]', @salt_bytes ) ); +	my $salt       = en_base64( pack( 'C[16]', @salt_bytes ) );  	return bcrypt( $password, '$2a$12$' . $salt );  } @@ -38,10 +38,10 @@ sub do_login {  	else {  		if ( $self->authenticate( $user, $password ) ) {  			$self->redirect_to( $self->req->param('redirect_to') // '/' ); -			$self->mark_seen( $self->current_user->{id} ); +			$self->users->mark_seen( uid => $self->current_user->{id} );  		}  		else { -			my $data = $self->get_user_password($user); +			my $data = $self->users->get_login_data( name => $user );  			if ( $data and $data->{status} == 0 ) {  				$self->render( 'login', invalid => 'confirmation' );  			} @@ -95,12 +95,12 @@ sub register {  		return;  	} -	if ( $self->check_if_user_name_exists($user) ) { +	if ( $self->users->check_if_user_name_exists( name => $user ) ) {  		$self->render( 'register', invalid => 'user_collision' );  		return;  	} -	if ( $self->check_if_mail_is_blacklisted($email) ) { +	if ( $self->users->check_if_mail_is_blacklisted( email => $email ) ) {  		$self->render( 'register', invalid => 'mail_blacklisted' );  		return;  	} @@ -115,11 +115,17 @@ sub register {  		return;  	} -	my $token       = make_token(); -	my $pw_hash     = hash_password($password); -	my $db          = $self->pg->db; -	my $tx          = $db->begin; -	my $user_id     = $self->add_user( $db, $user, $email, $token, $pw_hash ); +	my $token   = make_token(); +	my $pw_hash = hash_password($password); +	my $db      = $self->pg->db; +	my $tx      = $db->begin; +	my $user_id = $self->users->add_user( +		db            => $db, +		name          => $user, +		email         => $email, +		token         => $token, +		password_hash => $pw_hash +	);  	my $reg_url     = $self->url_for('reg')->to_abs->scheme('https');  	my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https'); @@ -164,7 +170,13 @@ sub verify {  		return;  	} -	if ( not $self->verify_registration_token( $id, $token ) ) { +	if ( +		not $self->users->verify_registration_token( +			uid   => $id, +			token => $token +		) +	  ) +	{  		$self->render( 'register', invalid => 'token' );  		return;  	} @@ -190,10 +202,10 @@ sub delete {  			$self->render( 'account', invalid => 'deletion password' );  			return;  		} -		$self->flag_user_deletion( $self->current_user->{id} ); +		$self->users->flag_deletion( uid => $self->current_user->{id} );  	}  	else { -		$self->unflag_user_deletion( $self->current_user->{id} ); +		$self->users->unflag_deletion( uid => $self->current_user->{id} );  	}  	$self->redirect_to('account');  } @@ -249,7 +261,10 @@ sub privacy {  			$public_level &= ~0x30;  		} -		$self->set_privacy( $user->{id}, $public_level ); +		$self->users->set_privacy( +			uid   => $user->{id}, +			level => $public_level +		);  		$self->flash( success => 'privacy' );  		$self->redirect_to('account'); @@ -274,7 +289,7 @@ sub insight {  	my ($self) = @_;  	my $user        = $self->current_user; -	my $use_history = $self->account_use_history( $user->{id} ); +	my $use_history = $self->users->use_history( uid => $user->{id} );  	if ( $self->param('action') and $self->param('action') eq 'save' ) {  		if ( $self->param('on_departure') ) { @@ -291,7 +306,10 @@ sub insight {  			$use_history &= ~0x02;  		} -		$self->account_use_history( $user->{id}, $use_history ); +		$self->users->use_history( +			uid => $user->{id}, +			set => $use_history +		);  		$self->flash( success => 'use_history' );  		$self->redirect_to('account');  	} @@ -375,8 +393,12 @@ sub change_mail {  		my $db    = $self->pg->db;  		my $tx    = $db->begin; -		$self->mark_for_mail_change( $db, $self->current_user->{id}, -			$email, $token ); +		$self->users->mark_for_mail_change( +			db    => $db, +			uid   => $self->current_user->{id}, +			email => $email, +			token => $token +		);  		my $ip   = $self->req->headers->header('X-Forwarded-For');  		my $ua   = $self->req->headers->user_agent; @@ -459,7 +481,10 @@ sub change_password {  	}  	my $pw_hash = hash_password($password); -	$self->set_user_password( $self->current_user->{id}, $pw_hash ); +	$self->users->set_password_hash( +		uid           => $self->current_user->{id}, +		password_hash => $pw_hash +	);  	$self->flash( success => 'password' );  	$self->redirect_to('account'); @@ -500,7 +525,10 @@ sub request_password_reset {  		my $name  = $self->param('user');  		my $email = $self->param('email'); -		my $uid = $self->get_uid_by_name_and_mail( $name, $email ); +		my $uid = $self->users->get_uid_by_name_and_mail( +			name  => $name, +			email => $email +		);  		if ( not $uid ) {  			$self->render( 'recover_password', @@ -512,7 +540,11 @@ sub request_password_reset {  		my $db    = $self->pg->db;  		my $tx    = $db->begin; -		my $error = $self->mark_for_password_reset( $db, $uid, $token ); +		my $error = $self->users->mark_for_password_reset( +			db    => $db, +			uid   => $uid, +			token => $token +		);  		if ($error) {  			$self->render( 'recover_password', invalid => $error ); @@ -570,7 +602,13 @@ sub request_password_reset {  			$self->render( 'set_password', invalid => 'csrf' );  			return;  		} -		if ( not $self->verify_password_token( $id, $token ) ) { +		if ( +			not $self->users->verify_password_token( +				uid   => $id, +				token => $token +			) +		  ) +		{  			$self->render( 'recover_password', invalid => 'change token' );  			return;  		} @@ -585,7 +623,10 @@ sub request_password_reset {  		}  		my $pw_hash = hash_password($password); -		$self->set_user_password( $id, $pw_hash ); +		$self->users->set_password_hash( +			uid           => $id, +			password_hash => $pw_hash +		);  		my $account = $self->get_user_data($id); @@ -597,7 +638,10 @@ sub request_password_reset {  		$self->flash( success => 'password' );  		$self->redirect_to('account'); -		$self->remove_password_token( $id, $token ); +		$self->users->remove_password_token( +			uid   => $id, +			token => $token +		);  		my $user  = $account->{name};  		my $email = $account->{email}; @@ -641,7 +685,13 @@ sub recover_password {  		return;  	} -	if ( $self->verify_password_token( $id, $token ) ) { +	if ( +		$self->users->verify_password_token( +			uid   => $id, +			token => $token +		) +	  ) +	{  		$self->render('set_password');  	}  	else { @@ -654,7 +704,13 @@ sub confirm_mail {  	my $id     = $self->current_user->{id};  	my $token  = $self->stash('token'); -	if ( $self->change_mail_with_token( $id, $token ) ) { +	if ( +		$self->users->change_mail_with_token( +			uid   => $id, +			token => $token +		) +	  ) +	{  		$self->flash( success => 'mail' );  		$self->redirect_to('account');  	} @@ -667,7 +723,7 @@ sub account {  	my ($self) = @_;  	$self->render('account'); -	$self->mark_seen( $self->current_user->{id} ); +	$self->users->mark_seen( uid => $self->current_user->{id} );  }  sub json_export { @@ -678,7 +734,7 @@ sub json_export {  	$self->render(  		json => { -			account => $db->select( 'users', '*', { id => $uid } )->hash, +			account    => $db->select( 'users', '*', { id => $uid } )->hash,  			in_transit => [  				$db->select( 'in_transit_str', '*', { user_id => $uid } )  				  ->hashes->each diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 702a89e..05bbccd 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -19,7 +19,7 @@ sub homepage {  			with_autocomplete => 1,  			with_geolocation  => 1  		); -		$self->mark_seen( $self->current_user->{id} ); +		$self->users->mark_seen( uid => $self->current_user->{id} );  	}  	else {  		$self->render( @@ -35,7 +35,7 @@ sub user_status {  	my $name = $self->stash('name');  	my $ts   = $self->stash('ts') // 0; -	my $user = $self->get_privacy_by_name($name); +	my $user = $self->users->get_privacy_by_name( name => $name );  	if ( not $user or not $user->{public_level} & 0x03 ) {  		$self->render('not_found'); @@ -150,7 +150,7 @@ sub public_status_card {  	my ($self) = @_;  	my $name = $self->stash('name'); -	my $user = $self->get_privacy_by_name($name); +	my $user = $self->users->get_privacy_by_name( name => $name );  	delete $self->stash->{layout}; @@ -457,7 +457,7 @@ sub station {  			title            => "travelynx: $status->{station_name}",  		);  	} -	$self->mark_seen( $self->current_user->{id} ); +	$self->users->mark_seen( uid => $self->current_user->{id} );  }  sub redirect_to_station { diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm new file mode 100644 index 0000000..830ec63 --- /dev/null +++ b/lib/Travelynx/Model/Users.pm @@ -0,0 +1,434 @@ +package Travelynx::Model::Users; + +use strict; +use warnings; +use 5.020; + +use DateTime; + +sub new { +	my ( $class, %opt ) = @_; + +	return bless( \%opt, $class ); +} + +sub mark_seen { +	my ($self, %opt) = @_; +	my $uid = $opt{uid}; +	my $db = $opt{db} // $self->{pg}->db; + +	$db->update( +		'users', +		{ last_seen => DateTime->now( time_zone => 'Europe/Berlin' ) }, +		{ id        => $uid } +	); +} + +sub verify_registration_token { +	my ( $self, %opt ) = @_; +	my $uid = $opt{uid}; +	my $token = $opt{token}; +	my $db = $opt{db} // $self->{pg}->db; + +	my $tx = $db->begin; + +	my $res = $db->select( +		'pending_registrations', +		'count(*) as count', +		{ +			user_id => $uid, +			token   => $token +		} +	); + +	if ( $res->hash->{count} ) { +		$db->update( 'users', { status => 1 }, { id => $uid } ); +		$db->delete( 'pending_registrations', { user_id => $uid } ); +		$tx->commit; +		return 1; +	} +	return; +} + +sub get_uid_by_name_and_mail { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $name = $opt{name}; +	my $email = $opt{email}; + +	my $res = $db->select( +		'users', +		['id'], +		{ +			name   => $name, +			email  => $email, +			status => 1 +		} +	); + +	if ( my $user = $res->hash ) { +		return $user->{id}; +	} +	return; +} + +sub get_privacy_by_name { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $name = $opt{name}; + +	my $res = $db->select( +		'users', +		[ 'id', 'public_level' ], +		{ +			name   => $name, +			status => 1 +		} +	); + +	if ( my $user = $res->hash ) { +		return $user; +	} +	return; +} + +sub set_privacy { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $public_level = $opt{level}; + +	$db->update( +		'users', +		{ public_level => $public_level }, +		{ id           => $uid } +	); +} + +sub mark_for_password_reset { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $token = $opt{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; +} + +sub verify_password_token { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $token = $opt{token}; + +	my $res = $db->select( +		'pending_passwords', +		'count(*) as count', +		{ +			user_id => $uid, +			token   => $token +		} +	); + +	if ( $res->hash->{count} ) { +		return 1; +	} +	return; +} + +sub mark_for_mail_change { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $email = $opt{email}; +	my $token = $opt{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' +		}, +	); +} + +sub change_mail_with_token { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $token = $opt{token}; + +	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; +} + +sub remove_password_token { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $token = $opt{token}; + +	$db->delete( +		'pending_passwords', +		{ +			user_id => $uid, +			token   => $token +		} +	); +} + +sub get_data { +	my ($self, %opt) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; + +	my $user = $db->select( +		'users', +		'id, name, status, public_level, email, ' +			. 'extract(epoch from registered_at) as registered_at_ts, ' +			. 'extract(epoch from last_seen) as last_seen_ts, ' +			. 'extract(epoch from deletion_requested) as deletion_requested_ts', +		{ id => $uid } +	)->hash; +	if ($user) { +		return { +			id            => $user->{id}, +			name          => $user->{name}, +			status        => $user->{status}, +			is_public     => $user->{public_level}, +			email         => $user->{email}, +			registered_at => DateTime->from_epoch( +				epoch     => $user->{registered_at_ts}, +				time_zone => 'Europe/Berlin' +			), +			last_seen => DateTime->from_epoch( +				epoch     => $user->{last_seen_ts}, +				time_zone => 'Europe/Berlin' +			), +			deletion_requested => $user->{deletion_requested_ts} +			? DateTime->from_epoch( +				epoch     => $user->{deletion_requested_ts}, +				time_zone => 'Europe/Berlin' +				) +			: undef, +		}; +	} +	return undef; +} + +sub get_login_data { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $name = $opt{name}; + +	my $res_h = $db->select( +		'users', +		'id, name, status, password as password_hash', +		{ name => $name } +	)->hash; + +	return $res_h; +} + +sub add_user { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $user_name = $opt{name}; +	my $email = $opt{email}; +	my $token = $opt{token}; +	my $password = $opt{password_hash}; + +	# This helper must be called during a transaction, as user creation +	# may fail even after the database entry has been generated, e.g.  if +	# the registration mail cannot be sent. We therefore use $db (the +	# database handle performing the transaction) instead of $self->pg->db +	# (which may be a new handle not belonging to the transaction). + +	my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + +	my $res = $db->insert( +		'users', +		{ +			name          => $user_name, +			status        => 0, +			public_level  => 0, +			email         => $email, +			password      => $password, +			registered_at => $now, +			last_seen     => $now, +		}, +		{ returning => 'id' } +	); +	my $uid = $res->hash->{id}; + +	$db->insert( +		'pending_registrations', +		{ +			user_id => $uid, +			token   => $token +		} +	); + +	return $uid; +} + +sub flag_deletion { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; + +	my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + +	$db->update( +		'users', +		{ deletion_requested => $now }, +		{ +			id => $uid, +		} +	); +} + +sub unflag_deletion { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; + +	$db->update( +		'users', +		{ +			deletion_requested => undef, +		}, +		{ +			id => $uid, +		} +	); +} + +sub set_password_hash { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $password = $opt{password_hash}; + +	$db->update( +		'users', +		{ password => $password }, +		{ id       => $uid } +	); +} + +sub check_if_user_name_exists { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $user_name = $opt{name}; + +	my $count = $db->select( +		'users', +		'count(*) as count', +		{ name => $user_name } +	)->hash->{count}; + +	if ($count) { +		return 1; +	} +	return 0; +} + +sub check_if_mail_is_blacklisted { +	my ( $self, %opt ) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $mail = $opt{email}; + +	my $count = $db->select( +		'users', +		'count(*) as count', +		{ +			email  => $mail, +			status => 0, +		} +	)->hash->{count}; + +	if ($count) { +		return 1; +	} + +	$count = $db->select( +		'mail_blacklist', +		'count(*) as count', +		{ +			email     => $mail, +			num_tries => { '>', 1 }, +		} +	)->hash->{count}; + +	if ($count) { +		return 1; +	} +	return 0; +} + +sub use_history { +	my ($self, %opt) = @_; +	my $db = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; +	my $value = $opt{set}; + +	if ($value) { +		$db->update( +			'users', +			{ use_history => $value }, +			{ id          => $uid } +		); +	} +	else { +		return $db->select( 'users', ['use_history'], +			{ id => $uid } )->hash->{use_history}; +	} +} + +1; | 
