Loading index.pl +163 −7 Original line number Diff line number Diff line Loading @@ -37,8 +37,13 @@ my %action_type = ( checkout => 2, undo => 3, ); my @action_types = (qw(checkin checkout undo)); my %token_type = ( status => 1, history => 2, action => 3, ); my @token_types = (qw(status history action)); app->plugin( authentication => { Loading Loading @@ -274,6 +279,57 @@ app->attr( ); } ); app->attr( get_api_tokens_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ select type, token from tokens where user_id = ? } ); } ); app->attr( get_api_token_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ select token from tokens where user_id = ? and type = ? } ); } ); app->attr( drop_api_token_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ delete from tokens where user_id = ? and type = ? } ); } ); app->attr( set_api_token_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ insert or replace into tokens (user_id, type, token) values (?, ?, ?) } ); } ); app->attr( get_password_query => sub { my ($self) = @_; Loading Loading @@ -635,6 +691,18 @@ helper 'get_user_data' => sub { return undef; }; helper 'get_api_token' => sub { my ( $self, $uid ) = @_; $uid //= $self->current_user->{id}; $self->app->get_api_tokens_query->execute($uid); my $rows = $self->app->get_api_tokens_query->fetchall_arrayref; my $token = {}; for my $row ( @{$rows} ) { $token->{ $token_types[ $row->[0] - 1 ] } = $row->[1]; } return $token; }; helper 'get_user_password' => sub { my ( $self, $name ) = @_; my $query = $self->app->get_password_query; Loading Loading @@ -770,9 +838,9 @@ helper 'get_user_travels' => sub { }; helper 'get_user_status' => sub { my ($self) = @_; my ( $self, $uid ) = @_; my $uid = $self->current_user->{id}; $uid //= $self->current_user->{id}; $self->app->get_last_actions_query->execute($uid); my $rows = $self->app->get_last_actions_query->fetchall_arrayref; Loading @@ -784,7 +852,9 @@ helper 'get_user_status' => sub { @cols = @{ $rows->[2] }; } my $ts = epoch_to_dt( $cols[1] ); my $action_ts = epoch_to_dt( $cols[1] ); my $sched_ts = epoch_to_dt( $cols[8] ); my $real_ts = epoch_to_dt( $cols[9] ); my $checkin_station_name = decode( 'UTF-8', $cols[3] ); my @route = split( qr{[|]}, decode( 'UTF-8', $cols[10] // q{} ) ); my @route_after; Loading @@ -799,8 +869,10 @@ helper 'get_user_status' => sub { } return { checked_in => ( $cols[0] == $action_type{checkin} ), timestamp => $ts, timestamp_delta => $now->epoch - $ts->epoch, timestamp => $action_ts, timestamp_delta => $now->epoch - $action_ts->epoch, sched_ts => $sched_ts, real_ts => $real_ts, station_ds100 => $cols[2], station_name => $checkin_station_name, train_type => $cols[4], Loading @@ -813,7 +885,9 @@ helper 'get_user_status' => sub { } return { checked_in => 0, timestamp => 0 timestamp => epoch_to_dt(0), sched_ts => epoch_to_dt(0), real_ts => epoch_to_dt(0), }; }; Loading Loading @@ -914,6 +988,63 @@ post '/geolocation' => sub { }; get '/api/v0/:action/:token' => sub { my ($self) = @_; my $api_action = $self->stash('action'); my $api_token = $self->stash('token'); if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) { $self->render( json => { error => 'Invalid action', }, ); return; } if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) { $self->render( json => { error => 'Malformed token', }, ); return; } my $uid = $+{id}; $api_token = $+{token}; my $token = $self->get_api_token($uid); if ( $api_token ne $token->{$api_action} ) { $self->render( json => { error => 'Invalid token', }, ); return; } if ( $api_action eq 'status' ) { my $status = $self->get_user_status($uid); $self->render( json => { checked_in => $status->{checked_in} ? \1 : \0, station_ds100 => $status->{station_ds100}, station_name => $status->{station_name}, train_type => $status->{train_type}, train_line => $status->{train_line}, train_no => $status->{train_no}, action_ts => $status->{timestamp}->epoch, sched_ts => $status->{sched_ts}->epoch, real_ts => $status->{real_ts}->epoch, }, ); } else { $self->render( json => { error => 'not implemented', }, ); } }; get '/login' => sub { my ($self) = @_; $self->render('login'); Loading Loading @@ -1287,6 +1418,31 @@ post '/logout' => sub { $self->redirect_to('/login'); }; post '/set_token' => sub { my ($self) = @_; if ( $self->validation->csrf_protect->has_error('csrf_token') ) { $self->render( 'account', invalid => 'csrf' ); return; } my $token = make_token(); my $token_id = $token_type{ $self->param('token') }; if ( not $token_id ) { $self->redirect_to('account'); return; } if ( $self->param('action') eq 'delete' ) { $self->app->drop_api_token_query->execute( $self->current_user->{id}, $token_id ); } else { $self->app->set_api_token_query->execute( $self->current_user->{id}, $token_id, $token ); } $self->redirect_to('account'); }; get '/s/*station' => sub { my ($self) = @_; my $station = $self->stash('station'); Loading migrate.pl +21 −0 Original line number Diff line number Diff line Loading @@ -174,6 +174,27 @@ my @migrations = ( ); $dbh->commit; }, # v2 -> v3 sub { $dbh->begin_work; $dbh->do( qq{ update schema_version set version = 3; } ); $dbh->do( qq{ create table tokens ( user_id integer not null, type integer not null, token char(80) not null, primary key (user_id, type) ); } ); $dbh->commit; }, ); my $schema_version = get_schema_version(); Loading templates/account.html.ep +119 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,125 @@ </div> </div> <h1>API</h1> % my $token = get_api_token(); <div class="row"> <div class="col s12"> <p> Die folgenden API-Token erlauben den passwortlosen automatisierten Zugriff auf API-Endpunkte. Bitte umsichtig behandeln – sobald ein Token gesetzt ist, können mit Kenntnis von Token und Nutzer-ID alle zugehörigen API-Aktionen ausgeführt werden. Logindaten sind dazu nicht erforderlich. </p> <table class="striped"> <tr> <th scope="row">Status</th> <td> % if ($token->{status}) { %= $acc->{id} . '-' . $token->{status} % } % else { — % } </td> <td> %= form_for 'set_token' => begin %= csrf_field %= hidden_field 'token' => 'status' <button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> Generieren </button> <button class="btn waves-effect waves-light red" type="submit" name="action" value="delete"> Löschen </button> %= end </td> </tr> <tr> <th scope="row">History</th> <td> % if ($token->{history}) { %= $acc->{id} . '-' . $token->{history} % } % else { — % } </td> <td> %= form_for 'set_token' => begin %= csrf_field %= hidden_field 'token' => 'history' <button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> Generieren </button> <button class="btn waves-effect waves-light red" type="submit" name="action" value="delete"> Löschen </button> %= end </td> </tr> <tr> <th scope="row">Travel</th> <td> % if ($token->{action}) { %= $acc->{id} . '-' . $token->{action} % } % else { — % } </td> <td> %= form_for 'set_token' => begin %= csrf_field %= hidden_field 'token' => 'action' <button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> Generieren </button> <button class="btn waves-effect waves-light red" type="submit" name="action" value="delete"> Löschen </button> %= end </td> </tr> </table> </div> </div> <h2>Status</h2> % my $api_root = $self->url_for('/api/v0')->to_abs->scheme('https'); <div class="row"> <div class="col s12"> <p> Das Format der API v0 kann sich noch ändern, ab v1 ist es stabil. </p> <p style="font-family: Monospace;"> % if ($token->{status}) { curl <%= $api_root %>/status/<%= $acc->{id} %>-<%= $token->{status} // 'TOKEN' %> % } % else { curl <%= $api_root %>/status/TOKEN % } </p> <p style="font-family: Monospace;"> {<br/> "checked_in" : true / false,<br/> "station_ds100" : "EE", (DS100-Kürzel der letzten Station)<br/> "station_name" : "Essen Hbf", (Name der letzten Station)<br/> "train_type" : "ICE", (aktueller / letzter Zugtyp)<br/> "train_line" : "", (Linie, ggf. null)<br/> "train_no" : "1234", (Zugnummer)<br/> "action_ts" : 1234567, (UNIX-Timestamp des letzten Checkin/Checkout)<br/> "sched_ts" : 1234567, (UNIX-Timestamp der zugehörigen Ankunft/Abfahrt gemäß Fahrplan. Ggf. 0)<br/> "real_ts" : 1234567, (UNIX-Timestamp der zugehörigen Ankunft/Abfahrt laut Echtzeitdaten. Ggf. 0)<br/> } </p> <p> Im Fehlerfall: <span style="font-family: Monospace;">{ "error" : "Begründung" }</span> </p> </div> </div> <h1>Export</h1> <div class="row"> Loading Loading
index.pl +163 −7 Original line number Diff line number Diff line Loading @@ -37,8 +37,13 @@ my %action_type = ( checkout => 2, undo => 3, ); my @action_types = (qw(checkin checkout undo)); my %token_type = ( status => 1, history => 2, action => 3, ); my @token_types = (qw(status history action)); app->plugin( authentication => { Loading Loading @@ -274,6 +279,57 @@ app->attr( ); } ); app->attr( get_api_tokens_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ select type, token from tokens where user_id = ? } ); } ); app->attr( get_api_token_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ select token from tokens where user_id = ? and type = ? } ); } ); app->attr( drop_api_token_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ delete from tokens where user_id = ? and type = ? } ); } ); app->attr( set_api_token_query => sub { my ($self) = @_; return $self->app->dbh->prepare( qq{ insert or replace into tokens (user_id, type, token) values (?, ?, ?) } ); } ); app->attr( get_password_query => sub { my ($self) = @_; Loading Loading @@ -635,6 +691,18 @@ helper 'get_user_data' => sub { return undef; }; helper 'get_api_token' => sub { my ( $self, $uid ) = @_; $uid //= $self->current_user->{id}; $self->app->get_api_tokens_query->execute($uid); my $rows = $self->app->get_api_tokens_query->fetchall_arrayref; my $token = {}; for my $row ( @{$rows} ) { $token->{ $token_types[ $row->[0] - 1 ] } = $row->[1]; } return $token; }; helper 'get_user_password' => sub { my ( $self, $name ) = @_; my $query = $self->app->get_password_query; Loading Loading @@ -770,9 +838,9 @@ helper 'get_user_travels' => sub { }; helper 'get_user_status' => sub { my ($self) = @_; my ( $self, $uid ) = @_; my $uid = $self->current_user->{id}; $uid //= $self->current_user->{id}; $self->app->get_last_actions_query->execute($uid); my $rows = $self->app->get_last_actions_query->fetchall_arrayref; Loading @@ -784,7 +852,9 @@ helper 'get_user_status' => sub { @cols = @{ $rows->[2] }; } my $ts = epoch_to_dt( $cols[1] ); my $action_ts = epoch_to_dt( $cols[1] ); my $sched_ts = epoch_to_dt( $cols[8] ); my $real_ts = epoch_to_dt( $cols[9] ); my $checkin_station_name = decode( 'UTF-8', $cols[3] ); my @route = split( qr{[|]}, decode( 'UTF-8', $cols[10] // q{} ) ); my @route_after; Loading @@ -799,8 +869,10 @@ helper 'get_user_status' => sub { } return { checked_in => ( $cols[0] == $action_type{checkin} ), timestamp => $ts, timestamp_delta => $now->epoch - $ts->epoch, timestamp => $action_ts, timestamp_delta => $now->epoch - $action_ts->epoch, sched_ts => $sched_ts, real_ts => $real_ts, station_ds100 => $cols[2], station_name => $checkin_station_name, train_type => $cols[4], Loading @@ -813,7 +885,9 @@ helper 'get_user_status' => sub { } return { checked_in => 0, timestamp => 0 timestamp => epoch_to_dt(0), sched_ts => epoch_to_dt(0), real_ts => epoch_to_dt(0), }; }; Loading Loading @@ -914,6 +988,63 @@ post '/geolocation' => sub { }; get '/api/v0/:action/:token' => sub { my ($self) = @_; my $api_action = $self->stash('action'); my $api_token = $self->stash('token'); if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) { $self->render( json => { error => 'Invalid action', }, ); return; } if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) { $self->render( json => { error => 'Malformed token', }, ); return; } my $uid = $+{id}; $api_token = $+{token}; my $token = $self->get_api_token($uid); if ( $api_token ne $token->{$api_action} ) { $self->render( json => { error => 'Invalid token', }, ); return; } if ( $api_action eq 'status' ) { my $status = $self->get_user_status($uid); $self->render( json => { checked_in => $status->{checked_in} ? \1 : \0, station_ds100 => $status->{station_ds100}, station_name => $status->{station_name}, train_type => $status->{train_type}, train_line => $status->{train_line}, train_no => $status->{train_no}, action_ts => $status->{timestamp}->epoch, sched_ts => $status->{sched_ts}->epoch, real_ts => $status->{real_ts}->epoch, }, ); } else { $self->render( json => { error => 'not implemented', }, ); } }; get '/login' => sub { my ($self) = @_; $self->render('login'); Loading Loading @@ -1287,6 +1418,31 @@ post '/logout' => sub { $self->redirect_to('/login'); }; post '/set_token' => sub { my ($self) = @_; if ( $self->validation->csrf_protect->has_error('csrf_token') ) { $self->render( 'account', invalid => 'csrf' ); return; } my $token = make_token(); my $token_id = $token_type{ $self->param('token') }; if ( not $token_id ) { $self->redirect_to('account'); return; } if ( $self->param('action') eq 'delete' ) { $self->app->drop_api_token_query->execute( $self->current_user->{id}, $token_id ); } else { $self->app->set_api_token_query->execute( $self->current_user->{id}, $token_id, $token ); } $self->redirect_to('account'); }; get '/s/*station' => sub { my ($self) = @_; my $station = $self->stash('station'); Loading
migrate.pl +21 −0 Original line number Diff line number Diff line Loading @@ -174,6 +174,27 @@ my @migrations = ( ); $dbh->commit; }, # v2 -> v3 sub { $dbh->begin_work; $dbh->do( qq{ update schema_version set version = 3; } ); $dbh->do( qq{ create table tokens ( user_id integer not null, type integer not null, token char(80) not null, primary key (user_id, type) ); } ); $dbh->commit; }, ); my $schema_version = get_schema_version(); Loading
templates/account.html.ep +119 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,125 @@ </div> </div> <h1>API</h1> % my $token = get_api_token(); <div class="row"> <div class="col s12"> <p> Die folgenden API-Token erlauben den passwortlosen automatisierten Zugriff auf API-Endpunkte. Bitte umsichtig behandeln – sobald ein Token gesetzt ist, können mit Kenntnis von Token und Nutzer-ID alle zugehörigen API-Aktionen ausgeführt werden. Logindaten sind dazu nicht erforderlich. </p> <table class="striped"> <tr> <th scope="row">Status</th> <td> % if ($token->{status}) { %= $acc->{id} . '-' . $token->{status} % } % else { — % } </td> <td> %= form_for 'set_token' => begin %= csrf_field %= hidden_field 'token' => 'status' <button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> Generieren </button> <button class="btn waves-effect waves-light red" type="submit" name="action" value="delete"> Löschen </button> %= end </td> </tr> <tr> <th scope="row">History</th> <td> % if ($token->{history}) { %= $acc->{id} . '-' . $token->{history} % } % else { — % } </td> <td> %= form_for 'set_token' => begin %= csrf_field %= hidden_field 'token' => 'history' <button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> Generieren </button> <button class="btn waves-effect waves-light red" type="submit" name="action" value="delete"> Löschen </button> %= end </td> </tr> <tr> <th scope="row">Travel</th> <td> % if ($token->{action}) { %= $acc->{id} . '-' . $token->{action} % } % else { — % } </td> <td> %= form_for 'set_token' => begin %= csrf_field %= hidden_field 'token' => 'action' <button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> Generieren </button> <button class="btn waves-effect waves-light red" type="submit" name="action" value="delete"> Löschen </button> %= end </td> </tr> </table> </div> </div> <h2>Status</h2> % my $api_root = $self->url_for('/api/v0')->to_abs->scheme('https'); <div class="row"> <div class="col s12"> <p> Das Format der API v0 kann sich noch ändern, ab v1 ist es stabil. </p> <p style="font-family: Monospace;"> % if ($token->{status}) { curl <%= $api_root %>/status/<%= $acc->{id} %>-<%= $token->{status} // 'TOKEN' %> % } % else { curl <%= $api_root %>/status/TOKEN % } </p> <p style="font-family: Monospace;"> {<br/> "checked_in" : true / false,<br/> "station_ds100" : "EE", (DS100-Kürzel der letzten Station)<br/> "station_name" : "Essen Hbf", (Name der letzten Station)<br/> "train_type" : "ICE", (aktueller / letzter Zugtyp)<br/> "train_line" : "", (Linie, ggf. null)<br/> "train_no" : "1234", (Zugnummer)<br/> "action_ts" : 1234567, (UNIX-Timestamp des letzten Checkin/Checkout)<br/> "sched_ts" : 1234567, (UNIX-Timestamp der zugehörigen Ankunft/Abfahrt gemäß Fahrplan. Ggf. 0)<br/> "real_ts" : 1234567, (UNIX-Timestamp der zugehörigen Ankunft/Abfahrt laut Echtzeitdaten. Ggf. 0)<br/> } </p> <p> Im Fehlerfall: <span style="font-family: Monospace;">{ "error" : "Begründung" }</span> </p> </div> </div> <h1>Export</h1> <div class="row"> Loading