summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rwxr-xr-xlib/Travelynx.pm85
-rw-r--r--lib/Travelynx/Command/database.pm17
-rw-r--r--lib/Travelynx/Command/maintenance.pm7
-rw-r--r--lib/Travelynx/Controller/Account.pm154
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) = @_;