Loading lib/Travelynx.pm +11 −402 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ use Travel::Status::DE::DBWagenreihung; use Travel::Status::DE::IRIS; use Travel::Status::DE::IRIS::Stations; use Travelynx::Helper::Sendmail; use Travelynx::Model::Users; use XML::LibXML; sub check_password { Loading Loading @@ -93,7 +94,8 @@ sub startup { }, validate_user => sub { my ( $self, $username, $password, $extradata ) = @_; my $user_info = $self->get_user_password($username); my $user_info = $self->users->get_login_data( name => $username ); if ( not $user_info ) { return undef; } Loading Loading @@ -270,6 +272,13 @@ sub startup { } ); $self->helper( users => sub { my ($self) = @_; state $users = Travelynx::Model::Users->new( pg => $self->pg ); } ); $self->helper( pg => sub { my ($self) = @_; Loading Loading @@ -918,18 +927,6 @@ sub startup { } ); $self->helper( 'mark_seen' => sub { my ( $self, $uid ) = @_; $self->pg->db->update( 'users', { last_seen => DateTime->now( time_zone => 'Europe/Berlin' ) }, { id => $uid } ); } ); $self->helper( 'update_in_transit_comment' => sub { my ( $self, $comment, $uid ) = @_; Loading Loading @@ -1151,197 +1148,6 @@ sub startup { } ); $self->helper( 'verify_registration_token' => sub { my ( $self, $uid, $token ) = @_; my $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; } ); $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( 'get_privacy_by_name' => sub { my ( $self, $name ) = @_; my $res = $self->pg->db->select( 'users', [ 'id', 'public_level' ], { name => $name, status => 1 } ); if ( my $user = $res->hash ) { return $user; } return; } ); $self->helper( 'set_privacy' => sub { my ( $self, $uid, $public_level ) = @_; $self->pg->db->update( 'users', { public_level => $public_level }, { id => $uid } ); } ); $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( '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 ) = @_; $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). Loading @@ -1351,39 +1157,7 @@ sub startup { $uid //= $self->current_user->{id}; my $user_data = $self->pg->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_data) { return { id => $user_data->{id}, name => $user_data->{name}, status => $user_data->{status}, is_public => $user_data->{public_level}, email => $user_data->{email}, registered_at => DateTime->from_epoch( epoch => $user_data->{registered_at_ts}, time_zone => 'Europe/Berlin' ), last_seen => DateTime->from_epoch( epoch => $user_data->{last_seen_ts}, time_zone => 'Europe/Berlin' ), deletion_requested => $user_data->{deletion_requested_ts} ? DateTime->from_epoch( epoch => $user_data->{deletion_requested_ts}, time_zone => 'Europe/Berlin' ) : undef, }; } return undef; return $self->users->get_data( uid => $uid ); } ); Loading Loading @@ -1534,153 +1308,6 @@ sub startup { } ); $self->helper( 'get_user_password' => sub { my ( $self, $name ) = @_; my $res_h = $self->pg->db->select( 'users', 'id, name, status, password as password_hash', { name => $name } )->hash; return $res_h; } ); $self->helper( 'add_user' => sub { my ( $self, $db, $user_name, $email, $token, $password ) = @_; # 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; } ); $self->helper( 'flag_user_deletion' => sub { my ( $self, $uid ) = @_; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); $self->pg->db->update( 'users', { deletion_requested => $now }, { id => $uid, } ); } ); $self->helper( 'unflag_user_deletion' => sub { my ( $self, $uid ) = @_; $self->pg->db->update( 'users', { deletion_requested => undef, }, { id => $uid, } ); } ); $self->helper( 'set_user_password' => sub { my ( $self, $uid, $password ) = @_; $self->pg->db->update( 'users', { password => $password }, { id => $uid } ); } ); $self->helper( 'check_if_user_name_exists' => sub { my ( $self, $user_name ) = @_; my $count = $self->pg->db->select( 'users', 'count(*) as count', { name => $user_name } )->hash->{count}; if ($count) { return 1; } return 0; } ); $self->helper( 'check_if_mail_is_blacklisted' => sub { my ( $self, $mail ) = @_; my $count = $self->pg->db->select( 'users', 'count(*) as count', { email => $mail, status => 0, } )->hash->{count}; if ($count) { return 1; } $count = $self->pg->db->select( 'mail_blacklist', 'count(*) as count', { email => $mail, num_tries => { '>', 1 }, } )->hash->{count}; if ($count) { return 1; } return 0; } ); $self->helper( 'delete_journey' => sub { my ( $self, $journey_id, $checkin_epoch, $checkout_epoch ) = @_; Loading Loading @@ -2910,24 +2537,6 @@ sub startup { } ); $self->helper( 'account_use_history' => sub { my ( $self, $uid, $value ) = @_; if ($value) { $self->pg->db->update( 'users', { use_history => $value }, { id => $uid } ); } else { return $self->pg->db->select( 'users', ['use_history'], { id => $uid } )->hash->{use_history}; } } ); $self->helper( 'get_user_travels' => sub { my ( $self, %opt ) = @_; Loading lib/Travelynx/Controller/Account.pm +84 −28 Original line number Diff line number Diff line Loading @@ -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' ); } Loading Loading @@ -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; } Loading @@ -119,7 +119,13 @@ sub register { 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 $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'); Loading Loading @@ -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; } Loading @@ -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'); } Loading Loading @@ -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'); Loading @@ -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') ) { Loading @@ -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'); } Loading Loading @@ -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; Loading Loading @@ -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'); Loading Loading @@ -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', Loading @@ -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 ); Loading Loading @@ -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; } Loading @@ -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); Loading @@ -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}; Loading Loading @@ -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 { Loading @@ -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'); } Loading @@ -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 { Loading lib/Travelynx/Controller/Traveling.pm +4 −4 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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'); Loading Loading @@ -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}; Loading Loading @@ -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 { Loading lib/Travelynx/Model/Users.pm 0 → 100644 +434 −0 Original line number Diff line number Diff line 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; templates/account.html.ep +1 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,7 @@ <h1>Account</h1> % my $acc = current_user(); % my $hook = get_webhook(); % my $use_history = account_use_history($acc->{id}); % my $use_history = users->use_history(uid => $acc->{id}); <div class="row"> <div class="col s12"> <table class="striped"> Loading Loading
lib/Travelynx.pm +11 −402 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ use Travel::Status::DE::DBWagenreihung; use Travel::Status::DE::IRIS; use Travel::Status::DE::IRIS::Stations; use Travelynx::Helper::Sendmail; use Travelynx::Model::Users; use XML::LibXML; sub check_password { Loading Loading @@ -93,7 +94,8 @@ sub startup { }, validate_user => sub { my ( $self, $username, $password, $extradata ) = @_; my $user_info = $self->get_user_password($username); my $user_info = $self->users->get_login_data( name => $username ); if ( not $user_info ) { return undef; } Loading Loading @@ -270,6 +272,13 @@ sub startup { } ); $self->helper( users => sub { my ($self) = @_; state $users = Travelynx::Model::Users->new( pg => $self->pg ); } ); $self->helper( pg => sub { my ($self) = @_; Loading Loading @@ -918,18 +927,6 @@ sub startup { } ); $self->helper( 'mark_seen' => sub { my ( $self, $uid ) = @_; $self->pg->db->update( 'users', { last_seen => DateTime->now( time_zone => 'Europe/Berlin' ) }, { id => $uid } ); } ); $self->helper( 'update_in_transit_comment' => sub { my ( $self, $comment, $uid ) = @_; Loading Loading @@ -1151,197 +1148,6 @@ sub startup { } ); $self->helper( 'verify_registration_token' => sub { my ( $self, $uid, $token ) = @_; my $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; } ); $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( 'get_privacy_by_name' => sub { my ( $self, $name ) = @_; my $res = $self->pg->db->select( 'users', [ 'id', 'public_level' ], { name => $name, status => 1 } ); if ( my $user = $res->hash ) { return $user; } return; } ); $self->helper( 'set_privacy' => sub { my ( $self, $uid, $public_level ) = @_; $self->pg->db->update( 'users', { public_level => $public_level }, { id => $uid } ); } ); $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( '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 ) = @_; $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). Loading @@ -1351,39 +1157,7 @@ sub startup { $uid //= $self->current_user->{id}; my $user_data = $self->pg->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_data) { return { id => $user_data->{id}, name => $user_data->{name}, status => $user_data->{status}, is_public => $user_data->{public_level}, email => $user_data->{email}, registered_at => DateTime->from_epoch( epoch => $user_data->{registered_at_ts}, time_zone => 'Europe/Berlin' ), last_seen => DateTime->from_epoch( epoch => $user_data->{last_seen_ts}, time_zone => 'Europe/Berlin' ), deletion_requested => $user_data->{deletion_requested_ts} ? DateTime->from_epoch( epoch => $user_data->{deletion_requested_ts}, time_zone => 'Europe/Berlin' ) : undef, }; } return undef; return $self->users->get_data( uid => $uid ); } ); Loading Loading @@ -1534,153 +1308,6 @@ sub startup { } ); $self->helper( 'get_user_password' => sub { my ( $self, $name ) = @_; my $res_h = $self->pg->db->select( 'users', 'id, name, status, password as password_hash', { name => $name } )->hash; return $res_h; } ); $self->helper( 'add_user' => sub { my ( $self, $db, $user_name, $email, $token, $password ) = @_; # 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; } ); $self->helper( 'flag_user_deletion' => sub { my ( $self, $uid ) = @_; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); $self->pg->db->update( 'users', { deletion_requested => $now }, { id => $uid, } ); } ); $self->helper( 'unflag_user_deletion' => sub { my ( $self, $uid ) = @_; $self->pg->db->update( 'users', { deletion_requested => undef, }, { id => $uid, } ); } ); $self->helper( 'set_user_password' => sub { my ( $self, $uid, $password ) = @_; $self->pg->db->update( 'users', { password => $password }, { id => $uid } ); } ); $self->helper( 'check_if_user_name_exists' => sub { my ( $self, $user_name ) = @_; my $count = $self->pg->db->select( 'users', 'count(*) as count', { name => $user_name } )->hash->{count}; if ($count) { return 1; } return 0; } ); $self->helper( 'check_if_mail_is_blacklisted' => sub { my ( $self, $mail ) = @_; my $count = $self->pg->db->select( 'users', 'count(*) as count', { email => $mail, status => 0, } )->hash->{count}; if ($count) { return 1; } $count = $self->pg->db->select( 'mail_blacklist', 'count(*) as count', { email => $mail, num_tries => { '>', 1 }, } )->hash->{count}; if ($count) { return 1; } return 0; } ); $self->helper( 'delete_journey' => sub { my ( $self, $journey_id, $checkin_epoch, $checkout_epoch ) = @_; Loading Loading @@ -2910,24 +2537,6 @@ sub startup { } ); $self->helper( 'account_use_history' => sub { my ( $self, $uid, $value ) = @_; if ($value) { $self->pg->db->update( 'users', { use_history => $value }, { id => $uid } ); } else { return $self->pg->db->select( 'users', ['use_history'], { id => $uid } )->hash->{use_history}; } } ); $self->helper( 'get_user_travels' => sub { my ( $self, %opt ) = @_; Loading
lib/Travelynx/Controller/Account.pm +84 −28 Original line number Diff line number Diff line Loading @@ -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' ); } Loading Loading @@ -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; } Loading @@ -119,7 +119,13 @@ sub register { 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 $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'); Loading Loading @@ -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; } Loading @@ -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'); } Loading Loading @@ -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'); Loading @@ -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') ) { Loading @@ -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'); } Loading Loading @@ -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; Loading Loading @@ -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'); Loading Loading @@ -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', Loading @@ -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 ); Loading Loading @@ -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; } Loading @@ -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); Loading @@ -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}; Loading Loading @@ -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 { Loading @@ -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'); } Loading @@ -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 { Loading
lib/Travelynx/Controller/Traveling.pm +4 −4 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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'); Loading Loading @@ -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}; Loading Loading @@ -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 { Loading
lib/Travelynx/Model/Users.pm 0 → 100644 +434 −0 Original line number Diff line number Diff line 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;
templates/account.html.ep +1 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,7 @@ <h1>Account</h1> % my $acc = current_user(); % my $hook = get_webhook(); % my $use_history = account_use_history($acc->{id}); % my $use_history = users->use_history(uid => $acc->{id}); <div class="row"> <div class="col s12"> <table class="striped"> Loading