parent
6ff397c9b3
commit
25d0530e86
8 changed files with 451 additions and 0 deletions
|
@ -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.
|
# 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
|
# If you don't have one, use current_user() instead (get_user_data will
|
||||||
# delegate to it anyways).
|
# delegate to it anyways).
|
||||||
|
@ -1530,6 +1612,8 @@ sub startup {
|
||||||
$r->get('/api/v0/:user_action/:token')->to('api#get_v0');
|
$r->get('/api/v0/:user_action/:token')->to('api#get_v0');
|
||||||
$r->get('/api/v1/:user_action/:token')->to('api#get_v1');
|
$r->get('/api/v1/:user_action/:token')->to('api#get_v1');
|
||||||
$r->get('/login')->to('account#login_form');
|
$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('/register')->to('account#registration_form');
|
||||||
$r->get('/reg/:id/:token')->to('account#verify');
|
$r->get('/reg/:id/:token')->to('account#verify');
|
||||||
$r->post('/action')->to('traveling#log_action');
|
$r->post('/action')->to('traveling#log_action');
|
||||||
|
@ -1537,6 +1621,7 @@ sub startup {
|
||||||
$r->post('/list_departures')->to('traveling#redirect_to_station');
|
$r->post('/list_departures')->to('traveling#redirect_to_station');
|
||||||
$r->post('/login')->to('account#do_login');
|
$r->post('/login')->to('account#do_login');
|
||||||
$r->post('/register')->to('account#register');
|
$r->post('/register')->to('account#register');
|
||||||
|
$r->post('/recover')->to('account#request_password_reset');
|
||||||
|
|
||||||
my $authed_r = $r->under(
|
my $authed_r = $r->under(
|
||||||
sub {
|
sub {
|
||||||
|
|
|
@ -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 {
|
sub setup_db {
|
||||||
|
|
|
@ -62,6 +62,13 @@ sub run {
|
||||||
printf( "Pruned unverified user %d\n", $user->{id} );
|
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'],
|
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;
|
||||||
|
|
|
@ -278,6 +278,160 @@ sub change_password {
|
||||||
$self->sendmail->custom( $email, 'travelynx: Passwort geändert', $body );
|
$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 {
|
sub account {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
|
|
|
@ -159,5 +159,45 @@ $t->post_ok(
|
||||||
);
|
);
|
||||||
$t->status_is(302)->header_is( location => '/' );
|
$t->status_is(302)->header_is( location => '/' );
|
||||||
|
|
||||||
|
$csrf_token = $t->ua->get('/account')->res->dom->at('input[name=csrf_token]')
|
||||||
|
->attr('value');
|
||||||
|
$t->post_ok(
|
||||||
|
'/logout' => form => {
|
||||||
|
csrf_token => $csrf_token,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$t->status_is(302)->header_is( location => '/login' );
|
||||||
|
|
||||||
|
$csrf_token = $t->ua->get('/recover')->res->dom->at('input[name=csrf_token]')
|
||||||
|
->attr('value');
|
||||||
|
$t->post_ok(
|
||||||
|
'/recover' => form => {
|
||||||
|
csrf_token => $csrf_token,
|
||||||
|
action => 'initiate',
|
||||||
|
user => 'someone',
|
||||||
|
email => 'foo@example.org',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$t->status_is(200)->content_like(qr{wird durchgeführt});
|
||||||
|
|
||||||
|
$res = $t->app->pg->db->select( 'pending_passwords', ['token'],
|
||||||
|
{ user_id => $uid } );
|
||||||
|
$token = $res->hash->{token};
|
||||||
|
|
||||||
|
$t->get_ok("/recover/${uid}/${token}")->status_is(200)
|
||||||
|
->content_like(qr{Neues Passwort eintragen});
|
||||||
|
|
||||||
|
$t->post_ok(
|
||||||
|
'/recover' => form => {
|
||||||
|
csrf_token => $csrf_token,
|
||||||
|
action => 'set_password',
|
||||||
|
id => $uid,
|
||||||
|
token => $token,
|
||||||
|
newpw => 'foofoofoo2',
|
||||||
|
newpw2 => 'foofoofoo2',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$t->status_is(302)->header_is( location => '/account' );
|
||||||
|
|
||||||
$t->app->pg->db->query('drop schema travelynx_test_02 cascade');
|
$t->app->pg->db->query('drop schema travelynx_test_02 cascade');
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|
|
@ -102,4 +102,15 @@
|
||||||
<div class="col s3 m3 l3">
|
<div class="col s3 m3 l3">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" style="margin-top: 2em;">
|
||||||
|
<div class="col s3 m3 l3">
|
||||||
|
</div>
|
||||||
|
<div class="col s6 m6 l6 center-align">
|
||||||
|
<a href="/recover">
|
||||||
|
Passwort vergessen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col s3 m3 l3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
%= end
|
%= end
|
||||||
|
|
77
templates/recover_password.html.ep
Normal file
77
templates/recover_password.html.ep
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<h1>Passwort zurücksetzen</h1>
|
||||||
|
% if (my $invalid = stash('invalid')) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card red darken-4">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
% if ($invalid eq 'csrf') {
|
||||||
|
<span class="card-title">Ungültiger CSRF-Token</span>
|
||||||
|
<p>Sind Cookies aktiviert? Ansonsten könnte es sich um einen
|
||||||
|
Fall von <a
|
||||||
|
href="https://de.wikipedia.org/wiki/Cross-Site-Request-Forgery">CSRF</a>
|
||||||
|
handeln.</p>
|
||||||
|
% }
|
||||||
|
% elsif ($invalid eq 'credentials') {
|
||||||
|
<span class="card-title">Ungültige Daten</span>
|
||||||
|
<p>Falscher Account oder falsches Mail-Adresse.</p>
|
||||||
|
% }
|
||||||
|
% elsif ($invalid eq 'in progress') {
|
||||||
|
<span class="card-title">Passwort-Reset wird durchgeführt</span>
|
||||||
|
<p>Es wurde bereits ein Reset-Link verschickt.</p>
|
||||||
|
% }
|
||||||
|
% elsif ($invalid eq 'token') {
|
||||||
|
<span class="card-title">Ungültiger Token</span>
|
||||||
|
<p>Der Reset-Token ist ungültig oder abgelaufen. Neuen beantragen?</p>
|
||||||
|
% }
|
||||||
|
% else {
|
||||||
|
<span class="card-title">Unbekannter Fehler</span>
|
||||||
|
<p>„<%= $invalid %>“</p>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
% 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">Passwort-Reset wird durchgeführt</span>
|
||||||
|
<p>
|
||||||
|
Ein für zwei Tage gültiger Reset-Link wurde an deine
|
||||||
|
Mail-Adresse verschickt. Sobald du damit ein neues Passwort
|
||||||
|
gesetzt hast, kannst du dich wieder anmelden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
%= form_for '/recover' => (method => 'POST') => begin
|
||||||
|
%= csrf_field
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">account_circle</i>
|
||||||
|
%= text_field 'user', id => 'user', class => 'validate', required => undef, maxlength => 60, autocomplete => 'username'
|
||||||
|
<label for="user">Account</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">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="initiate">
|
||||||
|
Passwort-Reset
|
||||||
|
<i class="material-icons right">send</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col s3 m3 l3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
%= end
|
60
templates/set_password.html.ep
Normal file
60
templates/set_password.html.ep
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
% if (my $invalid = stash('invalid')) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s12">
|
||||||
|
<div class="card red darken-4">
|
||||||
|
<div class="card-content white-text">
|
||||||
|
% if ($invalid eq 'csrf') {
|
||||||
|
<span class="card-title">Ungültiger CSRF-Token</span>
|
||||||
|
<p>Sind Cookies aktiviert? Ansonsten könnte es sich um einen
|
||||||
|
Fall von <a
|
||||||
|
href="https://de.wikipedia.org/wiki/Cross-Site-Request-Forgery">CSRF</a>
|
||||||
|
handeln.</p>
|
||||||
|
% }
|
||||||
|
% elsif ($invalid eq 'password_notequal') {
|
||||||
|
<span class="card-title">Passwort ungültig</span>
|
||||||
|
<p>Die angegebenen neuen Passwörter sind nicht identisch.</p>
|
||||||
|
% }
|
||||||
|
% elsif ($invalid eq 'password_short') {
|
||||||
|
<span class="card-title">Passwort zu kurz</span>
|
||||||
|
<p>Das neue Passwort muss mindestens acht Zeichen lang sein.</p>
|
||||||
|
% }
|
||||||
|
% else {
|
||||||
|
<span class="card-title">Unbekannter Fehler</span>
|
||||||
|
<p>„<%= $invalid %>“</p>
|
||||||
|
% }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% }
|
||||||
|
|
||||||
|
<h1>Neues Passwort eintragen</h1>
|
||||||
|
%= form_for '/recover' => (method => 'POST') => begin
|
||||||
|
%= csrf_field
|
||||||
|
%= hidden_field 'id' => param('id')
|
||||||
|
%= hidden_field 'token' => param('token')
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-field col l6 m12 s12">
|
||||||
|
<i class="material-icons prefix">lock</i>
|
||||||
|
%= password_field 'newpw', id => 'password', class => 'validate', required => undef, minlength => 8, autocomplete => 'new-password'
|
||||||
|
<label for="password">Neues Passwort</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col l6 m12 s12">
|
||||||
|
<i class="material-icons prefix">lock</i>
|
||||||
|
%= password_field 'newpw2', id => 'password2', class => 'validate', required => undef, minlength => 8, autocomplete => 'new-password'
|
||||||
|
<label for="password2">Passwort wiederholen</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="set_password">
|
||||||
|
Eintragen
|
||||||
|
<i class="material-icons right">send</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col s3 m3 l3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
%= end
|
Loading…
Reference in a new issue