parent
4ad1a1d20e
commit
867a5d4afb
7 changed files with 254 additions and 10 deletions
|
@ -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(
|
$self->helper(
|
||||||
'remove_password_token' => sub {
|
'remove_password_token' => sub {
|
||||||
my ( $self, $uid, $token ) = @_;
|
my ( $self, $uid, $token ) = @_;
|
||||||
|
@ -1004,7 +1055,7 @@ sub startup {
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = $self->pg->db->select(
|
$count = $self->pg->db->select(
|
||||||
'pending_mails',
|
'mail_blacklist',
|
||||||
'count(*) as count',
|
'count(*) as count',
|
||||||
{
|
{
|
||||||
email => $mail,
|
email => $mail,
|
||||||
|
@ -1638,6 +1689,7 @@ sub startup {
|
||||||
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
|
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
|
||||||
$authed_r->get('/cancelled')->to('traveling#cancelled');
|
$authed_r->get('/cancelled')->to('traveling#cancelled');
|
||||||
$authed_r->get('/change_password')->to('account#password_form');
|
$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('/export.json')->to('account#json_export');
|
||||||
$authed_r->get('/history.json')->to('traveling#json_history');
|
$authed_r->get('/history.json')->to('traveling#json_history');
|
||||||
$authed_r->get('/history')->to('traveling#history');
|
$authed_r->get('/history')->to('traveling#history');
|
||||||
|
@ -1646,9 +1698,11 @@ sub startup {
|
||||||
$authed_r->get('/journey/add')->to('traveling#add_journey_form');
|
$authed_r->get('/journey/add')->to('traveling#add_journey_form');
|
||||||
$authed_r->get('/journey/:id')->to('traveling#journey_details');
|
$authed_r->get('/journey/:id')->to('traveling#journey_details');
|
||||||
$authed_r->get('/s/*station')->to('traveling#station');
|
$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/add')->to('traveling#add_journey_form');
|
||||||
$authed_r->post('/journey/edit')->to('traveling#edit_journey');
|
$authed_r->post('/journey/edit')->to('traveling#edit_journey');
|
||||||
$authed_r->post('/change_password')->to('account#change_password');
|
$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('/delete')->to('account#delete');
|
||||||
$authed_r->post('/logout')->to('account#do_logout');
|
$authed_r->post('/logout')->to('account#do_logout');
|
||||||
$authed_r->post('/set_token')->to('api#set_token');
|
$authed_r->post('/set_token')->to('api#set_token');
|
||||||
|
|
|
@ -378,7 +378,7 @@ my @migrations = (
|
||||||
},
|
},
|
||||||
|
|
||||||
# v6 -> v7
|
# 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 {
|
sub {
|
||||||
my ($db) = @_;
|
my ($db) = @_;
|
||||||
$db->query(
|
$db->query(
|
||||||
|
@ -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 {
|
sub setup_db {
|
||||||
|
|
|
@ -34,13 +34,13 @@ sub run {
|
||||||
);
|
);
|
||||||
|
|
||||||
my $pending
|
my $pending
|
||||||
= $db->select( 'pending_mails', ['num_tries'], { email => $mail } );
|
= $db->select( 'mail_blacklist', ['num_tries'], { email => $mail } );
|
||||||
my $pending_h = $pending->hash;
|
my $pending_h = $pending->hash;
|
||||||
|
|
||||||
if ($pending_h) {
|
if ($pending_h) {
|
||||||
my $num_tries = $pending_h->{num_tries} + 1;
|
my $num_tries = $pending_h->{num_tries} + 1;
|
||||||
$db->update(
|
$db->update(
|
||||||
'pending_mails',
|
'mail_blacklist',
|
||||||
{
|
{
|
||||||
num_tries => $num_tries,
|
num_tries => $num_tries,
|
||||||
last_try => $reg_date
|
last_try => $reg_date
|
||||||
|
@ -50,7 +50,7 @@ sub run {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$db->insert(
|
$db->insert(
|
||||||
'pending_mails',
|
'mail_blacklist',
|
||||||
{
|
{
|
||||||
email => $mail,
|
email => $mail,
|
||||||
num_tries => 1,
|
num_tries => 1,
|
||||||
|
@ -69,6 +69,13 @@ sub run {
|
||||||
printf( "Pruned %d pending password reset(s)\n", $rows );
|
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'],
|
my $to_delete = $db->select( 'users', ['id'],
|
||||||
{ deletion_requested => { '<', $deletion_deadline } } );
|
{ deletion_requested => { '<', $deletion_deadline } } );
|
||||||
my @uids_to_delete = $to_delete->arrays->map( sub { shift->[0] } )->each;
|
my @uids_to_delete = $to_delete->arrays->map( sub { shift->[0] } )->each;
|
||||||
|
|
|
@ -211,6 +211,88 @@ sub do_logout {
|
||||||
$self->redirect_to('/login');
|
$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 {
|
sub password_form {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
|
@ -252,6 +334,7 @@ sub change_password {
|
||||||
my $pw_hash = hash_password($password);
|
my $pw_hash = hash_password($password);
|
||||||
$self->set_user_password( $self->current_user->{id}, $pw_hash );
|
$self->set_user_password( $self->current_user->{id}, $pw_hash );
|
||||||
|
|
||||||
|
$self->flash( success => 'password' );
|
||||||
$self->redirect_to('account');
|
$self->redirect_to('account');
|
||||||
|
|
||||||
my $user = $self->current_user->{name};
|
my $user = $self->current_user->{name};
|
||||||
|
@ -361,7 +444,7 @@ sub request_password_reset {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ( not $self->verify_password_token( $id, $token ) ) {
|
if ( not $self->verify_password_token( $id, $token ) ) {
|
||||||
$self->render( 'recover_password', invalid => 'recovery token' );
|
$self->render( 'recover_password', invalid => 'change token' );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ( $password ne $password2 ) {
|
if ( $password ne $password2 ) {
|
||||||
|
@ -384,6 +467,7 @@ sub request_password_reset {
|
||||||
invalid => 'Authentication failure – WTF?' );
|
invalid => 'Authentication failure – WTF?' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$self->flash( success => 'password' );
|
||||||
$self->redirect_to('account');
|
$self->redirect_to('account');
|
||||||
|
|
||||||
$self->remove_password_token( $id, $token );
|
$self->remove_password_token( $id, $token );
|
||||||
|
@ -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 {
|
sub account {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
|
|
|
@ -69,9 +69,9 @@
|
||||||
<span class="card-title">Passwort-Reset wird durchgeführt</span>
|
<span class="card-title">Passwort-Reset wird durchgeführt</span>
|
||||||
<p>Es wurde bereits ein Reset-Link verschickt.</p>
|
<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>
|
<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') {
|
% elsif ($invalid eq 'deletion password') {
|
||||||
<span class="card-title">Ungültiges Passwort</span>
|
<span class="card-title">Ungültiges Passwort</span>
|
||||||
|
|
|
@ -2,6 +2,23 @@
|
||||||
%= include '_invalid_input', invalid => $invalid
|
%= include '_invalid_input', invalid => $invalid
|
||||||
% }
|
% }
|
||||||
|
|
||||||
|
% if (my $success = flash('success')) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card green darken-4">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
% if ($success eq 'mail') {
|
||||||
|
<span class="card-title">Mail-Adresse erfolgreich geändert</span>
|
||||||
|
% }
|
||||||
|
% elsif ($success eq 'password') {
|
||||||
|
<span class="card-title">Passwort erfolgreich geändert</span>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
|
||||||
<h1>Account</h1>
|
<h1>Account</h1>
|
||||||
% my $acc = current_user();
|
% my $acc = current_user();
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -13,11 +30,11 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Mail</th>
|
<th scope="row">Mail</th>
|
||||||
<td><%= $acc->{email} %></td>
|
<td><%= $acc->{email} %><a href="/change_mail" style="margin-left: 1em;"><i class="material-icons">edit</i> ändern</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Passwort</th>
|
<th scope="row">Passwort</th>
|
||||||
<td><a href="/change_password" class="waves-effect waves-light btn">ändern</a></td>
|
<td><a href="/change_password"><i class="material-icons">edit</i> ändern</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Registriert am</th>
|
<th scope="row">Registriert am</th>
|
||||||
|
|
49
templates/change_mail.html.ep
Normal file
49
templates/change_mail.html.ep
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
% if (my $invalid = stash('invalid')) {
|
||||||
|
%= include '_invalid_input', invalid => $invalid
|
||||||
|
% }
|
||||||
|
|
||||||
|
% if (stash('success')) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card green darken-4">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
<span class="card-title">Mail-Änderung wird durchgeführt</span>
|
||||||
|
<p>
|
||||||
|
Ein für zwei Tage gültiger Bestätigungs-Link wurde an die
|
||||||
|
angegebene Mail-Adresse verschickt. Sobald du ihn aufgerufen
|
||||||
|
hast, wird die neue Adresse in deinem Account eingetragen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
|
||||||
|
<h1>Mail ändern</h1>
|
||||||
|
%= form_for '/change_mail' => (method => 'POST') => begin
|
||||||
|
%= csrf_field
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">lock</i>
|
||||||
|
%= password_field 'password', id => 'password', class => 'validate', required => undef, autocomplete => 'current-password'
|
||||||
|
<label for="password">Aktuelles Passwort</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">email</i>
|
||||||
|
%= email_field 'email', id => 'email', class => 'validate', required => undef, maxlength => 250
|
||||||
|
<label for="email">Neue Mail-Adresse</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s3 m3 l3">
|
||||||
|
</div>
|
||||||
|
<div class="col s6 m6 l6 center-align">
|
||||||
|
<button class="btn waves-effect waves-light" type="submit" name="action" value="update_mail">
|
||||||
|
Ändern
|
||||||
|
<i class="material-icons right">send</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col s3 m3 l3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
%= end
|
Loading…
Reference in a new issue