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 => { authentication => {
autoload_user => 1, autoload_user => 1,
session_key => 'foodor', session_key => 'foodor',
fail_render => { template => 'login' },
load_user => sub { load_user => sub {
my ( $self, $uid ) = @_; my ( $self, $uid ) = @_;
my $data = $self->get_user_data($uid); return $self->get_user_data($uid);
if ($data) {
return { name => $data->{name} };
}
return undef;
}, },
validate_user => sub { validate_user => sub {
my ( $self, $username, $password, $extradata ) = @_; my ( $self, $username, $password, $extradata ) = @_;
@ -68,6 +65,7 @@ app->plugin(
}, },
} }
); );
app->sessions->default_expiration( 60 * 60 * 24 * 180 );
app->defaults( layout => 'default' ); app->defaults( layout => 'default' );
@ -427,7 +425,7 @@ helper 'checkin' => sub {
} }
my $success = $self->app->checkin_query->execute( my $success = $self->app->checkin_query->execute(
$self->get_user_id, $self->current_user->{id},
$self->get_station_id( $self->get_station_id(
ds100 => $status->{station_ds100}, ds100 => $status->{station_ds100},
name => $status->{station_name} name => $status->{station_name}
@ -457,7 +455,7 @@ helper 'checkin' => sub {
helper 'undo' => sub { helper 'undo' => sub {
my ($self) = @_; my ($self) = @_;
my $uid = $self->get_user_id; my $uid = $self->current_user->{id};
$self->app->get_last_actions_query->execute($uid); $self->app->get_last_actions_query->execute($uid);
my $rows = $self->app->get_last_actions_query->fetchall_arrayref; my $rows = $self->app->get_last_actions_query->fetchall_arrayref;
@ -469,10 +467,10 @@ helper 'undo' => sub {
return 'Repeated undo is not supported'; return 'Repeated undo is not supported';
} }
my $success my $success = $self->app->undo_query->execute(
= $self->app->undo_query->execute( $self->get_user_id, $self->current_user->{id},
DateTime->now( time_zone => 'Europe/Berlin' )->epoch, DateTime->now( time_zone => 'Europe/Berlin' )->epoch,
); );
if ( defined $success ) { if ( defined $success ) {
return; return;
@ -501,7 +499,7 @@ helper 'checkout' => sub {
if ( not defined $train ) { if ( not defined $train ) {
if ($force) { if ($force) {
my $success = $self->app->checkout_query->execute( my $success = $self->app->checkout_query->execute(
$self->get_user_id, $self->current_user->{id},
$self->get_station_id( $self->get_station_id(
ds100 => $status->{station_ds100}, ds100 => $status->{station_ds100},
name => $status->{station_name} name => $status->{station_name}
@ -523,7 +521,7 @@ helper 'checkout' => sub {
} }
else { else {
my $success = $self->app->checkout_query->execute( my $success = $self->app->checkout_query->execute(
$self->get_user_id, $self->current_user->{id},
$self->get_station_id( $self->get_station_id(
ds100 => $status->{station_ds100}, ds100 => $status->{station_ds100},
name => $status->{station_name} name => $status->{station_name}
@ -580,7 +578,7 @@ helper 'get_user_token' => sub {
helper 'get_user_data' => sub { helper 'get_user_data' => sub {
my ( $self, $uid ) = @_; my ( $self, $uid ) = @_;
$uid //= $self->get_user_id; $uid //= $self->current_user->{id};
my $query = $self->app->get_user_query; my $query = $self->app->get_user_query;
$query->execute($uid); $query->execute($uid);
my $rows = $query->fetchall_arrayref; my $rows = $query->fetchall_arrayref;
@ -603,7 +601,7 @@ helper 'get_user_data' => sub {
deletion_requested => $row[7] deletion_requested => $row[7]
}; };
} }
return; return undef;
}; };
helper 'get_user_password' => sub { helper 'get_user_password' => sub {
@ -623,80 +621,9 @@ helper 'get_user_password' => sub {
return; return;
}; };
helper 'get_user_name' => sub { helper 'add_user' => sub {
my ($self) = @_;
my $user = $self->req->headers->header('X-Remote-User') // 'dev';
return $user;
};
helper 'get_user_id' => sub {
my ( $self, $user_name, $email, $token, $password ) = @_; 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); $self->app->get_userid_query->execute($user_name);
my $rows = $self->app->get_userid_query->fetchall_arrayref; 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 { helper 'get_user_travels' => sub {
my ( $self, $limit ) = @_; my ( $self, $limit ) = @_;
my $uid = $self->get_user_id; my $uid = $self->current_user->{id};
my $query = $self->app->get_all_actions_query; my $query = $self->app->get_all_actions_query;
if ($limit) { if ($limit) {
$query = $self->app->get_last_actions_query; $query = $self->app->get_last_actions_query;
@ -800,7 +727,7 @@ helper 'get_user_travels' => sub {
helper 'get_user_status' => sub { helper 'get_user_status' => sub {
my ($self) = @_; my ($self) = @_;
my $uid = $self->get_user_id; my $uid = $self->current_user->{id};
$self->app->get_last_actions_query->execute($uid); $self->app->get_last_actions_query->execute($uid);
my $rows = $self->app->get_last_actions_query->fetchall_arrayref; my $rows = $self->app->get_last_actions_query->fetchall_arrayref;
@ -886,7 +813,217 @@ helper 'navbar_class' => sub {
get '/' => sub { get '/' => sub {
my ($self) = @_; 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 { post '/action' => sub {
@ -979,15 +1116,21 @@ post '/action' => sub {
} }
}; };
get '/a/account' => sub { get '/account' => sub {
my ($self) = @_; my ($self) = @_;
$self->render('account'); $self->render('account');
}; };
get '/a/export.json' => sub { get '/history' => sub {
my ($self) = @_; 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; my $query = $self->app->get_all_actions_query;
$query->execute($uid); $query->execute($uid);
@ -1031,220 +1174,13 @@ get '/a/export.json' => sub {
); );
}; };
get '/a/history' => sub { post '/logout' => 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 {
my ($self) = @_; my ($self) = @_;
$self->logout; $self->logout;
$self->redirect_to('/x/login'); $self->redirect_to('/login');
}; };
get '/x/register' => sub { get '/s/*station' => 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 {
my ($self) = @_; my ($self) = @_;
my $station = $self->stash('station'); my $station = $self->stash('station');

View file

@ -25,14 +25,14 @@ $(document).ready(function() {
stationlink.attr('href', ds100); stationlink.attr('href', ds100);
stationlink.text(name); 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); placeholder.replaceWith(resultTable);
} }
}; };
var processLocation = function(loc) { 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) { 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'), station: link.data('station'),
force: link.data('force'), 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.append(' Ohne Echtzeitdaten auschecken?')
link.data('force', true); 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="row">
<div class="col s12"> <div class="col s12">
<ul> <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> </ul>
</div> </div>
</div> </div>

View file

@ -1,72 +1,77 @@
<div class="row"> % if (is_user_authenticated()) {
<div class="col s12"> <div class="row">
% my $status = $self->get_user_status; <div class="col s12">
% if ($status->{checked_in}) { % my $status = get_user_status();
<div class="card green darken-4"> % if ($status->{checked_in}) {
<div class="card-content white-text"> <div class="card green darken-4">
<span class="card-title">Hallo, <%= $self->get_user_name %>!</span> <div class="card-content white-text">
<p>Du bist gerade eingecheckt in <span class="card-title">Hallo, <%= current_user()->{name} %>!</span>
<%= $status->{train_type} %> <%= $status->{train_no} %> <p>Du bist gerade eingecheckt in
ab <%= $status->{station_name} %>. <%= $status->{train_type} %> <%= $status->{train_no} %>
% if ($status->{timestamp_delta} < 3600) { ab <%= $status->{station_name} %>.
<a class="action-undo"><i class="material-icons">undo</i> Rückgängig</a> % 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>
% } % }
</tbody> </p>
</table> <p>Auschecken?</p>
</div> <table>
</div> <tbody>
% } % my $is_after = 0;
% else { % for my $station (@{$status->{route_after}}) {
<div class="card grey darken-4"> <tr><td><a class="action-checkout" data-station="<%= $station %>"><%= $station %></a></td></tr>
<div class="card-content white-text"> % }
<span class="card-title">Hallo, <%= $self->get_user_name %>!</span> </tbody>
<p>Du bist gerade nicht eingecheckt.</p> </table>
<p class="geolocationhint">Stationen in der Umgebung:</p>
<div class="geolocation">
<div class="progress"><div class="indeterminate"></div></div>
</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> % else {
</tabel> <div class="card grey darken-4">
</div> <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 charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#673ab7"> <meta name="theme-color" content="#673ab7">
%= stylesheet '/static/css/materialize.min.css' % my $av = 'v1'; # asset version
%= stylesheet '/static/css/material-icons.css' %= stylesheet "/static/${av}/css/materialize.min.css"
%= stylesheet '/static/css/local.css' %= stylesheet "/static/${av}/css/material-icons.css"
%= javascript '/static/js/jquery-2.2.4.min.js' %= stylesheet "/static/${av}/css/local.css"
%= javascript '/static/js/materialize.min.js' %= javascript "/static/${av}/js/jquery-2.2.4.min.js"
%= javascript '/static/js/travelynx-actions.min.js' %= javascript "/static/${av}/js/materialize.min.js"
%= javascript "/static/${av}/js/travelynx-actions.min.js"
% if (stash('with_geolocation')) { % if (stash('with_geolocation')) {
%= javascript '/static/js/geolocation.min.js' %= javascript "/static/${av}/js/geolocation.min.js"
% } % }
</head> </head>
<body> <body>
@ -21,9 +22,11 @@
<div class="nav-wrapper container"> <div class="nav-wrapper container">
<a href="/" class="brand-logo left">travelynx</a> <a href="/" class="brand-logo left">travelynx</a>
<ul id="nav-mobile" class="right"> <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> % if (is_user_authenticated()) {
<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('/history') %>"><a href='/history' title="History"><i class="material-icons">history</i></a></li>
<li class="<%= navbar_class('/x/about') %>"><a href='/x/about' title="About"><i class="material-icons">info_outline</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> </ul>
</div> </div>
</nav> </nav>

View file

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

View file

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