switch from HTTP Auth to Cookie Auth

This commit is contained in:
Daniel Friesel 2019-03-07 18:36:11 +01:00
parent ba6b517e5b
commit fd60839116
11 changed files with 335 additions and 390 deletions

540
index.pl
View file

@ -44,13 +44,10 @@ app->plugin(
authentication => {
autoload_user => 1,
session_key => 'foodor',
fail_render => { template => 'login' },
load_user => sub {
my ( $self, $uid ) = @_;
my $data = $self->get_user_data($uid);
if ($data) {
return { name => $data->{name} };
}
return undef;
return $self->get_user_data($uid);
},
validate_user => sub {
my ( $self, $username, $password, $extradata ) = @_;
@ -68,6 +65,7 @@ app->plugin(
},
}
);
app->sessions->default_expiration( 60 * 60 * 24 * 180 );
app->defaults( layout => 'default' );
@ -427,7 +425,7 @@ helper 'checkin' => sub {
}
my $success = $self->app->checkin_query->execute(
$self->get_user_id,
$self->current_user->{id},
$self->get_station_id(
ds100 => $status->{station_ds100},
name => $status->{station_name}
@ -457,7 +455,7 @@ helper 'checkin' => sub {
helper 'undo' => sub {
my ($self) = @_;
my $uid = $self->get_user_id;
my $uid = $self->current_user->{id};
$self->app->get_last_actions_query->execute($uid);
my $rows = $self->app->get_last_actions_query->fetchall_arrayref;
@ -469,10 +467,10 @@ helper 'undo' => sub {
return 'Repeated undo is not supported';
}
my $success
= $self->app->undo_query->execute( $self->get_user_id,
my $success = $self->app->undo_query->execute(
$self->current_user->{id},
DateTime->now( time_zone => 'Europe/Berlin' )->epoch,
);
);
if ( defined $success ) {
return;
@ -501,7 +499,7 @@ helper 'checkout' => sub {
if ( not defined $train ) {
if ($force) {
my $success = $self->app->checkout_query->execute(
$self->get_user_id,
$self->current_user->{id},
$self->get_station_id(
ds100 => $status->{station_ds100},
name => $status->{station_name}
@ -523,7 +521,7 @@ helper 'checkout' => sub {
}
else {
my $success = $self->app->checkout_query->execute(
$self->get_user_id,
$self->current_user->{id},
$self->get_station_id(
ds100 => $status->{station_ds100},
name => $status->{station_name}
@ -580,7 +578,7 @@ helper 'get_user_token' => sub {
helper 'get_user_data' => sub {
my ( $self, $uid ) = @_;
$uid //= $self->get_user_id;
$uid //= $self->current_user->{id};
my $query = $self->app->get_user_query;
$query->execute($uid);
my $rows = $query->fetchall_arrayref;
@ -603,7 +601,7 @@ helper 'get_user_data' => sub {
deletion_requested => $row[7]
};
}
return;
return undef;
};
helper 'get_user_password' => sub {
@ -623,80 +621,9 @@ helper 'get_user_password' => sub {
return;
};
helper 'get_user_name' => sub {
my ($self) = @_;
my $user = $self->req->headers->header('X-Remote-User') // 'dev';
return $user;
};
helper 'get_user_id' => sub {
helper 'add_user' => sub {
my ( $self, $user_name, $email, $token, $password ) = @_;
$user_name //= $self->get_user_name;
if ( not -e $dbname ) {
$self->app->dbh->begin_work;
$self->app->dbh->do(
qq{
create table schema_version (
version integer primary key
);
}
);
$self->app->dbh->do(
qq{
create table users (
id integer primary key,
name char(64) not null unique,
status int not null,
public_level bool not null,
email char(256),
token char(80),
password text,
registered_at datetime not null,
last_login datetime not null,
deletion_requested datetime
)
}
);
$self->app->dbh->do(
qq{
create table stations (
id integer primary key,
ds100 char(16) not null unique,
name char(64) not null unique
)
}
);
$self->app->dbh->do(
qq{
create table user_actions (
user_id int not null,
action_id int not null,
station_id int,
action_time int not null,
train_type char(16),
train_line char(16),
train_no char(16),
train_id char(128),
sched_time int,
real_time int,
route text,
messages text,
primary key (user_id, action_time)
)
}
);
$self->app->dbh->do(
qq{
insert into schema_version (version) values (1);
}
);
$self->app->dbh->commit;
}
$self->app->get_userid_query->execute($user_name);
my $rows = $self->app->get_userid_query->fetchall_arrayref;
@ -737,7 +664,7 @@ helper 'check_if_user_name_exists' => sub {
helper 'get_user_travels' => sub {
my ( $self, $limit ) = @_;
my $uid = $self->get_user_id;
my $uid = $self->current_user->{id};
my $query = $self->app->get_all_actions_query;
if ($limit) {
$query = $self->app->get_last_actions_query;
@ -800,7 +727,7 @@ helper 'get_user_travels' => sub {
helper 'get_user_status' => sub {
my ($self) = @_;
my $uid = $self->get_user_id;
my $uid = $self->current_user->{id};
$self->app->get_last_actions_query->execute($uid);
my $rows = $self->app->get_last_actions_query->fetchall_arrayref;
@ -886,7 +813,217 @@ helper 'navbar_class' => sub {
get '/' => sub {
my ($self) = @_;
$self->render( 'landingpage', with_geolocation => 1 );
if ( $self->is_user_authenticated ) {
$self->render( 'landingpage', with_geolocation => 1 );
}
else {
$self->render( 'landingpage', intro => 1 );
}
};
get '/about' => sub {
my ($self) = @_;
$self->render( 'about', version => $VERSION );
};
get '/impressum' => sub {
my ($self) = @_;
$self->render('imprint');
};
get '/imprint' => sub {
my ($self) = @_;
$self->render('imprint');
};
post '/geolocation' => sub {
my ($self) = @_;
my $lon = $self->param('lon');
my $lat = $self->param('lat');
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
}
else {
my @candidates = map {
{
ds100 => $_->[0][0],
name => $_->[0][1],
eva => $_->[0][2],
lon => $_->[0][3],
lat => $_->[0][4],
distance => $_->[1],
}
} Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
$lat, 5 );
$self->render(
json => {
candidates => [@candidates],
}
);
}
};
get '/login' => sub {
my ($self) = @_;
$self->render('login');
};
post '/login' => sub {
my ($self) = @_;
my $user = $self->req->param('user');
my $password = $self->req->param('password');
# Keep cookies for 6 months
$self->session( expiration => 60 * 60 * 24 * 180 );
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'login',
invalid => 'csrf',
);
}
else {
if ( $self->authenticate( $user, $password ) ) {
$self->redirect_to('/');
}
else {
$self->render( 'login', invalid => 'credentials' );
}
}
};
get '/register' => sub {
my ($self) = @_;
$self->render('register');
};
post '/register' => sub {
my ($self) = @_;
my $user = $self->req->param('user');
my $email = $self->req->param('email');
my $password = $self->req->param('password');
my $password2 = $self->req->param('password2');
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 );
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'register',
invalid => 'csrf',
);
return;
}
if ( not length($user) ) {
$self->render( 'register', invalid => 'user_empty' );
return;
}
if ( not length($email) ) {
$self->render( 'register', invalid => 'mail_empty' );
return;
}
if ( $user !~ m{ ^ [0-9a-zA-Z_-]+ $ }x ) {
$self->render( 'register', invalid => 'user_format' );
return;
}
if ( $self->check_if_user_name_exists($user) ) {
$self->render( 'register', invalid => 'user_collision' );
return;
}
if ( $password ne $password2 ) {
$self->render( 'register', invalid => 'password_notequal' );
return;
}
if ( length($password) < 8 ) {
$self->render( 'register', invalid => 'password_short' );
return;
}
my $token = make_token();
my $pw_hash = hash_password($password);
my $user_id = $self->add_user( $user, $email, $token, $pw_hash );
my $body = "Hallo, ${user}!\n\n";
$body .= "Mit deiner E-Mail-Adresse (${email}) wurde ein Account auf\n";
$body .= "travelynx.finalrewind.org angelegt.\n\n";
$body
.= "Falls die Registrierung von dir ausging, kannst du den Account unter\n";
$body .= "https://travelynx.finalrewind.org/reg/${user_id}/${token}\n";
$body .= "freischalten.\n\n";
$body
.= "Falls nicht, ignoriere diese Mail bitte. Nach 48 Stunden wird deine\n";
$body
.= "Mail-Adresse erneut zur Registrierung freigeschaltet. Falls auch diese fehlschlägt,\n";
$body
.= "werden wir sie dauerhaft sperren und keine Mails mehr dorthin schicken.\n\n";
$body .= "Daten zur Registrierung:\n";
$body .= " * Datum: ${date}\n";
$body .= " * Verwendete IP: ${ip}\n";
$body .= " * Verwendeter Browser gemäß User Agent: ${ua}\n\n\n";
$body .= "Impressum: https://travelynx.finalrewind.org/impressum\n";
my $reg_mail = Email::Simple->create(
header => [
To => $email,
From => 'Travelynx <travelynx@finalrewind.org>',
Subject => 'Registrierung auf travelynx.finalrewind.org',
'Content-Type' => 'text/plain; charset=UTF-8',
],
body => encode( 'utf-8', $body ),
);
my $success = try_to_sendmail($reg_mail);
if ($success) {
$self->render( 'login', from => 'register' );
}
else {
$self->render( 'register', invalid => 'sendmail' );
}
};
get '/reg/:id/:token' => sub {
my ($self) = @_;
my $id = $self->stash('id');
my $token = $self->stash('token');
my @db_user = $self->get_user_token($id);
if ( not @db_user ) {
$self->render( 'register', invalid => 'token' );
return;
}
my ( $db_name, $db_status, $db_token ) = @db_user;
if ( not $db_name or $token ne $db_token or $db_status != 0 ) {
$self->render( 'register', invalid => 'token' );
return;
}
$self->app->set_status_query->execute( 1, $id );
$self->render( 'login', from => 'verification' );
};
under sub {
my ($self) = @_;
return $self->is_user_authenticated;
};
post '/action' => sub {
@ -979,15 +1116,21 @@ post '/action' => sub {
}
};
get '/a/account' => sub {
get '/account' => sub {
my ($self) = @_;
$self->render('account');
};
get '/a/export.json' => sub {
get '/history' => sub {
my ($self) = @_;
my $uid = $self->get_user_id;
$self->render('history');
};
get '/export.json' => sub {
my ($self) = @_;
my $uid = $self->current_user->{id};
my $query = $self->app->get_all_actions_query;
$query->execute($uid);
@ -1031,220 +1174,13 @@ get '/a/export.json' => sub {
);
};
get '/a/history' => sub {
my ($self) = @_;
$self->render('history');
};
get '/x/about' => sub {
my ($self) = @_;
$self->render( 'about', version => $VERSION );
};
get '/x/impressum' => sub {
my ($self) = @_;
$self->render('imprint');
};
get '/x/imprint' => sub {
my ($self) = @_;
$self->render('imprint');
};
post '/x/geolocation' => sub {
my ($self) = @_;
my $lon = $self->param('lon');
my $lat = $self->param('lat');
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
}
else {
my @candidates = map {
{
ds100 => $_->[0][0],
name => $_->[0][1],
eva => $_->[0][2],
lon => $_->[0][3],
lat => $_->[0][4],
distance => $_->[1],
}
} Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
$lat, 5 );
$self->render(
json => {
candidates => [@candidates],
}
);
}
};
get '/x/login' => sub {
my ($self) = @_;
$self->render('login');
};
post '/x/login' => sub {
my ($self) = @_;
my $user = $self->req->param('user');
my $password = $self->req->param('password');
# Keep cookies for 6 months
$self->session( expiration => 60 * 60 * 24 * 180 );
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'login',
invalid => 'csrf',
);
}
else {
if ( $self->authenticate( $user, $password ) ) {
$self->redirect_to('/');
}
else {
$self->render( 'login', invalid => 'credentials' );
}
}
};
get '/x/logout' => sub {
post '/logout' => sub {
my ($self) = @_;
$self->logout;
$self->redirect_to('/x/login');
$self->redirect_to('/login');
};
get '/x/register' => sub {
my ($self) = @_;
$self->render('register');
};
post '/x/register' => sub {
my ($self) = @_;
my $user = $self->req->param('user');
my $email = $self->req->param('email');
my $password = $self->req->param('password');
my $password2 = $self->req->param('password2');
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 );
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'register',
invalid => 'csrf',
);
return;
}
if ( not length($user) ) {
$self->render( 'register', invalid => 'user_empty' );
return;
}
if ( not length($email) ) {
$self->render( 'register', invalid => 'mail_empty' );
return;
}
if ( $user !~ m{ ^ [0-9a-zA-Z_-]+ $ }x ) {
$self->render( 'register', invalid => 'user_format' );
return;
}
#if ( $self->check_if_user_name_exists($user) or $user eq 'dev' ) {
if ( $user ne $self->get_user_name ) {
$self->render( 'register', invalid => 'user_collision' );
return;
}
if ( $password ne $password2 ) {
$self->render( 'register', invalid => 'password_notequal' );
return;
}
if ( length($password) < 8 ) {
$self->render( 'register', invalid => 'password_short' );
return;
}
my $token = make_token();
my $pw_hash = hash_password($password);
my $user_id = $self->get_user_id( $user, $email, $token, $pw_hash );
my $body = "Hallo, ${user}!\n\n";
$body .= "Mit deiner E-Mail-Adresse (${email}) wurde ein Account auf\n";
$body .= "travelynx.finalrewind.org angelegt.\n\n";
$body
.= "Falls die Registrierung von dir ausging, kannst du den Account unter\n";
$body .= "https://travelynx.finalrewind.org/x/reg/${user_id}/${token}\n";
$body .= "freischalten.\n\n";
$body
.= "Falls nicht, ignoriere diese Mail bitte. Nach 48 Stunden wird deine\n";
$body
.= "Mail-Adresse erneut zur Registrierung freigeschaltet. Falls auch diese fehlschlägt,\n";
$body
.= "werden wir sie dauerhaft sperren und keine Mails mehr dorthin schicken.\n\n";
$body .= "Daten zur Registrierung:\n";
$body .= " * Datum: ${date}\n";
$body .= " * Verwendete IP: ${ip}\n";
$body .= " * Verwendeter Browser gemäß User Agent: ${ua}\n\n\n";
$body .= "Impressum: https://travelynx.finalrewind.org/x/impressum\n";
my $reg_mail = Email::Simple->create(
header => [
To => $email,
From => 'Travelynx <travelynx@finalrewind.org>',
Subject => 'Registrierung auf travelynx.finalrewind.org',
'Content-Type' => 'text/plain; charset=UTF-8',
],
body => encode( 'utf-8', $body ),
);
my $success = try_to_sendmail($reg_mail);
if ($success) {
$self->render( 'login', from => 'register' );
}
else {
$self->render( 'register', invalid => 'sendmail' );
}
};
get '/x/reg/:id/:token' => sub {
my ($self) = @_;
my $id = $self->stash('id');
my $token = $self->stash('token');
my @db_user = $self->get_user_token($id);
if ( not @db_user ) {
$self->render( 'register', invalid => 'token' );
return;
}
my ( $db_name, $db_status, $db_token ) = @db_user;
if ( not $db_name or $token ne $db_token or $db_status != 0 ) {
$self->render( 'register', invalid => 'token' );
return;
}
$self->app->set_status_query->execute( 1, $id );
$self->render( 'login', from => 'verification' );
};
get '/*station' => sub {
get '/s/*station' => sub {
my ($self) = @_;
my $station = $self->stash('station');

View file

@ -25,14 +25,14 @@ $(document).ready(function() {
stationlink.attr('href', ds100);
stationlink.text(name);
resultBody.append('<tr><td><a href="/' + ds100 + '">' + name + '</a></td></tr>');
resultBody.append('<tr><td><a href="/s/' + ds100 + '">' + name + '</a></td></tr>');
});
placeholder.replaceWith(resultTable);
}
};
var processLocation = function(loc) {
$.post('/x/geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude}, processResult);
$.post('/geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude}, processResult);
};
var processError = function(error) {

View file

@ -1 +1 @@
$(document).ready(function(){var e=$("p.geolocationhint"),t=$("div.geolocation div.progress"),o=function(o,a,n){e.remove(),t.remove()},a=function(e){e.error?o(0,e.error):0==e.candidates.length?o():(resultTable=$("<table><tbody></tbody></table>"),resultBody=resultTable.children(),$.each(e.candidates,function(e,t){var o=t.ds100,a=t.name,n=t.distance;n=n.toFixed(1);var r=$(document.createElement("a"));r.attr("href",o),r.text(a),resultBody.append('<tr><td><a href="/'+o+'">'+a+"</a></td></tr>")}),t.replaceWith(resultTable))},n=function(e){$.post("/x/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},a)},r=function(e){e.code==e.PERMISSION_DENIED?o():e.code==e.POSITION_UNAVAILABLE?o():(e.code,e.TIMEOUT,o())};navigator.geolocation?navigator.geolocation.getCurrentPosition(n,r):o()});
$(document).ready(function(){var a=$("p.geolocationhint"),n=$("div.geolocation div.progress"),t=function(e,t,o){a.remove(),n.remove()},o=function(e){e.error?t(0,e.error):0==e.candidates.length?t():(resultTable=$("<table><tbody></tbody></table>"),resultBody=resultTable.children(),$.each(e.candidates,function(e,t){var o=t.ds100,a=t.name,n=t.distance;n=n.toFixed(1);var r=$(document.createElement("a"));r.attr("href",o),r.text(a),resultBody.append('<tr><td><a href="/s/'+o+'">'+a+"</a></td></tr>")}),n.replaceWith(resultTable))};navigator.geolocation?navigator.geolocation.getCurrentPosition(function(e){$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},o)},function(e){e.code==e.PERMISSION_DENIED||e.code==e.POSITION_UNAVAILABLE||(e.code,e.TIMEOUT),t()}):t()});

View file

@ -34,7 +34,7 @@ $(document).ready(function() {
station: link.data('station'),
force: link.data('force'),
};
tvly_run(link, req, '/' + req.station, function() {
tvly_run(link, req, '/s/' + req.station, function() {
link.append(' Ohne Echtzeitdaten auschecken?')
link.data('force', true);
});

View file

@ -1 +1 @@
function tvly_run(n,t,i,a){var c='<i class="material-icons">error</i>',o=$('<div class="progress"><div class="indeterminate"></div></div>');n.hide(),n.after(o),$.post("/action",t,function(t){t.success?$(location).attr("href",i):(M.toast({html:c+" "+t.error}),o.remove(),a&&a(),n.append(" "+c),n.show())})}$(document).ready(function(){$(".action-checkin").click(function(){var t=$(this);tvly_run(t,{action:"checkin",station:t.data("station"),train:t.data("train")},"/")}),$(".action-checkout").click(function(){var t=$(this),n={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,n,"/"+n.station,function(){t.append(" Ohne Echtzeitdaten auschecken?"),t.data("force",!0)})}),$(".action-undo").click(function(){tvly_run($(this),{action:"undo"},window.location.href)})});
function tvly_run(n,t,i,a){var c='<i class="material-icons">error</i>',o=$('<div class="progress"><div class="indeterminate"></div></div>');n.hide(),n.after(o),$.post("/action",t,function(t){t.success?$(location).attr("href",i):(M.toast({html:c+" "+t.error}),o.remove(),a&&a(),n.append(" "+c),n.show())})}$(document).ready(function(){$(".action-checkin").click(function(){var t=$(this);tvly_run(t,{action:"checkin",station:t.data("station"),train:t.data("train")},"/")}),$(".action-checkout").click(function(){var t=$(this),n={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,n,"/s/"+n.station,function(){t.append(" Ohne Echtzeitdaten auschecken?"),t.data("force",!0)})}),$(".action-undo").click(function(){tvly_run($(this),{action:"undo"},window.location.href)})});

1
public/static/v1 Symbolic link
View file

@ -0,0 +1 @@
.

View file

@ -22,7 +22,7 @@
<div class="row">
<div class="col s12">
<ul>
<li><a href="/a/export.json">Rohdaten</a> (Kein API-Ersatz, das Format kann sich jederzeit ändern)</li>
<li><a href="/export.json">Rohdaten</a> (Kein API-Ersatz, das Format kann sich jederzeit ändern)</li>
</ul>
</div>
</div>

View file

@ -1,72 +1,77 @@
<div class="row">
<div class="col s12">
% my $status = $self->get_user_status;
% if ($status->{checked_in}) {
<div class="card green darken-4">
<div class="card-content white-text">
<span class="card-title">Hallo, <%= $self->get_user_name %>!</span>
<p>Du bist gerade eingecheckt in
<%= $status->{train_type} %> <%= $status->{train_no} %>
ab <%= $status->{station_name} %>.
% if ($status->{timestamp_delta} < 3600) {
<a class="action-undo"><i class="material-icons">undo</i> Rückgängig</a>
% }
</p>
<p>Auschecken?</p>
<table>
<tbody>
% my $is_after = 0;
% for my $station (@{$status->{route_after}}) {
<tr><td><a class="action-checkout" data-station="<%= $station %>"><%= $station %></a></td></tr>
% if (is_user_authenticated()) {
<div class="row">
<div class="col s12">
% my $status = get_user_status();
% if ($status->{checked_in}) {
<div class="card green darken-4">
<div class="card-content white-text">
<span class="card-title">Hallo, <%= current_user()->{name} %>!</span>
<p>Du bist gerade eingecheckt in
<%= $status->{train_type} %> <%= $status->{train_no} %>
ab <%= $status->{station_name} %>.
% if ($status->{timestamp_delta} < 3600) {
<a class="action-undo"><i class="material-icons">undo</i> Rückgängig</a>
% }
</tbody>
</table>
</div>
</div>
% }
% else {
<div class="card grey darken-4">
<div class="card-content white-text">
<span class="card-title">Hallo, <%= $self->get_user_name %>!</span>
<p>Du bist gerade nicht eingecheckt.</p>
<p class="geolocationhint">Stationen in der Umgebung:</p>
<div class="geolocation">
<div class="progress"><div class="indeterminate"></div></div>
</p>
<p>Auschecken?</p>
<table>
<tbody>
% my $is_after = 0;
% for my $station (@{$status->{route_after}}) {
<tr><td><a class="action-checkout" data-station="<%= $station %>"><%= $station %></a></td></tr>
% }
</tbody>
</table>
</div>
</div>
</div>
% }
</div>
</div>
<h1>Letzte Fahrten</h1>
<div class="row">
<table class="striped">
<thead>
<tr>
<th>Datum</th>
<th>Zug</th>
<th>Strecke</th>
<th>Dauer</th>
</tr>
</thead>
<tbody>
% for my $travel (get_user_travels(1)) {
% if ($travel->{completed}) {
<tr>
<td><%= $travel->{sched_departure}->strftime('%d.%m.%Y') %></td>
<td><%= $travel->{type} %> <%= $travel->{line} // $travel->{no} %></td>
<td><%= $travel->{from_name} %> → <%= $travel->{to_name} %></td>
% if ($travel->{rt_arrival}->epoch and $travel->{rt_departure}->epoch) {
<td><%= ($travel->{rt_arrival}->epoch - $travel->{rt_departure}->epoch) / 60 %> min
</td>
% } else {
<td><%= sprintf('%.f', $self->get_travel_distance($travel->{from_name}, $travel->{to_name}, $travel->{route})) %>km
<i class="material-icons">timer_off</i>
</td>
% }
</tr>
% }
% }
</tbody>
</tabel>
</div>
% else {
<div class="card grey darken-4">
<div class="card-content white-text">
<span class="card-title">Hallo, <%= current_user()->{name} %>!</span>
<p>Du bist gerade nicht eingecheckt.</p>
<p class="geolocationhint">Stationen in der Umgebung:</p>
<div class="geolocation">
<div class="progress"><div class="indeterminate"></div></div>
</div>
</div>
</div>
% }
</div>
</div>
<h1>Letzte Fahrten</h1>
<div class="row">
<table class="striped">
<thead>
<tr>
<th>Datum</th>
<th>Zug</th>
<th>Strecke</th>
<th>Dauer</th>
</tr>
</thead>
<tbody>
% for my $travel (get_user_travels(1)) {
% if ($travel->{completed}) {
<tr>
<td><%= $travel->{sched_departure}->strftime('%d.%m.%Y') %></td>
<td><%= $travel->{type} %> <%= $travel->{line} // $travel->{no} %></td>
<td><%= $travel->{from_name} %> → <%= $travel->{to_name} %></td>
% if ($travel->{rt_arrival}->epoch and $travel->{rt_departure}->epoch) {
<td><%= ($travel->{rt_arrival}->epoch - $travel->{rt_departure}->epoch) / 60 %> min
</td>
% } else {
<td><%= sprintf('%.f', $self->get_travel_distance($travel->{from_name}, $travel->{to_name}, $travel->{route})) %>km
<i class="material-icons">timer_off</i>
</td>
% }
</tr>
% }
% }
</tbody>
</tabel>
</div>
% }
% else {
Huhu!
% }

View file

@ -5,14 +5,15 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#673ab7">
%= stylesheet '/static/css/materialize.min.css'
%= stylesheet '/static/css/material-icons.css'
%= stylesheet '/static/css/local.css'
%= javascript '/static/js/jquery-2.2.4.min.js'
%= javascript '/static/js/materialize.min.js'
%= javascript '/static/js/travelynx-actions.min.js'
% my $av = 'v1'; # asset version
%= stylesheet "/static/${av}/css/materialize.min.css"
%= stylesheet "/static/${av}/css/material-icons.css"
%= stylesheet "/static/${av}/css/local.css"
%= javascript "/static/${av}/js/jquery-2.2.4.min.js"
%= javascript "/static/${av}/js/materialize.min.js"
%= javascript "/static/${av}/js/travelynx-actions.min.js"
% if (stash('with_geolocation')) {
%= javascript '/static/js/geolocation.min.js'
%= javascript "/static/${av}/js/geolocation.min.js"
% }
</head>
<body>
@ -21,9 +22,11 @@
<div class="nav-wrapper container">
<a href="/" class="brand-logo left">travelynx</a>
<ul id="nav-mobile" class="right">
<li class="<%= navbar_class('/a/history') %>"><a href='/a/history' title="History"><i class="material-icons">history</i></a></li>
<li class="<%= navbar_class('/a/account') %>"><a href="/a/account" title="Account"><i class="material-icons">account_circle</i></a></li>
<li class="<%= navbar_class('/x/about') %>"><a href='/x/about' title="About"><i class="material-icons">info_outline</i></a></li>
% if (is_user_authenticated()) {
<li class="<%= navbar_class('/history') %>"><a href='/history' title="History"><i class="material-icons">history</i></a></li>
<li class="<%= navbar_class('/account') %>"><a href="/account" title="Account"><i class="material-icons">account_circle</i></a></li>
% }
<li class="<%= navbar_class('/about') %>"><a href='/about' title="About"><i class="material-icons">info_outline</i></a></li>
</ul>
</div>
</nav>

View file

@ -7,7 +7,7 @@
<p>
Du bist bereits angemeldet. Falls du mehrere Accounts hast
und auf einen anderen wechseln möchtest, musst du dich
vorher <a href="/x/logout">abmelden</a>.
vorher <a href="/logout">abmelden</a>.
</p>
</div>
</div>
@ -64,7 +64,7 @@
</div>
% }
<div class="row">
%= form_for '/x/login' => (class => 'col s12', method => 'POST') => begin
%= form_for '/login' => (class => 'col s12', method => 'POST') => begin
%= csrf_field
<div class="row">
<div class="input-field col s12">

View file

@ -59,7 +59,7 @@
</div>
% }
<div class="row">
%= form_for '/x/register' => (class => 'col s12', method => 'POST') => begin
%= form_for '/register' => (class => 'col s12', method => 'POST') => begin
%= csrf_field
<div class="row">
<div class="input-field col l6 m12 s12">
@ -103,7 +103,7 @@
Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung
und für die "Passwort vergessen"-Funktionalität verwendet und nicht
an Dritte weitergegeben. Die <a
href="/x/impressum">Datenschutzerklärung</a> beschreibt weitere
href="/impressum">Datenschutzerklärung</a> beschreibt weitere
erhobene Daten sowie deren Zweck und Speicherfristen.
Accounts werden nach einem Jahr ohne Nutzung automatisch gelöscht.
</p>