Loading lib/Travelynx.pm +55 −1 Original line number Diff line number Diff line Loading @@ -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 ) = @_; Loading Loading @@ -1004,7 +1055,7 @@ sub startup { } $count = $self->pg->db->select( 'pending_mails', 'mail_blacklist', 'count(*) as count', { email => $mail, Loading Loading @@ -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'); Loading @@ -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'); Loading lib/Travelynx/Command/database.pm +20 −1 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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 { Loading lib/Travelynx/Command/maintenance.pm +10 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -50,7 +50,7 @@ sub run { } else { $db->insert( 'pending_mails', 'mail_blacklist', { email => $mail, num_tries => 1, Loading @@ -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; Loading lib/Travelynx/Controller/Account.pm +99 −1 Original line number Diff line number Diff line Loading @@ -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) = @_; Loading Loading @@ -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}; Loading Loading @@ -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 ) { Loading @@ -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 ); Loading Loading @@ -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) = @_; Loading templates/_invalid_input.html.ep +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading
lib/Travelynx.pm +55 −1 Original line number Diff line number Diff line Loading @@ -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 ) = @_; Loading Loading @@ -1004,7 +1055,7 @@ sub startup { } $count = $self->pg->db->select( 'pending_mails', 'mail_blacklist', 'count(*) as count', { email => $mail, Loading Loading @@ -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'); Loading @@ -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'); Loading
lib/Travelynx/Command/database.pm +20 −1 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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 { Loading
lib/Travelynx/Command/maintenance.pm +10 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -50,7 +50,7 @@ sub run { } else { $db->insert( 'pending_mails', 'mail_blacklist', { email => $mail, num_tries => 1, Loading @@ -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; Loading
lib/Travelynx/Controller/Account.pm +99 −1 Original line number Diff line number Diff line Loading @@ -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) = @_; Loading Loading @@ -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}; Loading Loading @@ -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 ) { Loading @@ -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 ); Loading Loading @@ -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) = @_; Loading
templates/_invalid_input.html.ep +2 −2 Original line number Diff line number Diff line Loading @@ -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