From d6264b5ca8a0192c02bb3aff676fd8aebe87c29e Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Sat, 16 Mar 2019 13:56:56 +0100 Subject: [PATCH] Add JSON API --- index.pl | 170 ++++++++++++++++++++++++++++++++++++-- migrate.pl | 21 +++++ templates/account.html.ep | 119 ++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 7 deletions(-) diff --git a/index.pl b/index.pl index 4ddb44e..066cf0b 100755 --- a/index.pl +++ b/index.pl @@ -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 => { @@ -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) = @_; @@ -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; @@ -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; @@ -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; @@ -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], @@ -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), }; }; @@ -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{ ^ (? \d+ ) - (? .* ) $ }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'); @@ -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'); diff --git a/migrate.pl b/migrate.pl index 3b4e8dc..d52715c 100755 --- a/migrate.pl +++ b/migrate.pl @@ -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(); diff --git a/templates/account.html.ep b/templates/account.html.ep index 74af719..b23c9af 100644 --- a/templates/account.html.ep +++ b/templates/account.html.ep @@ -33,6 +33,125 @@ +

API

+% my $token = get_api_token(); +
+
+

+ 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. +

+ + + + + + + + + + + + + + + + +
Status + % if ($token->{status}) { + %= $acc->{id} . '-' . $token->{status} + % } + % else { + — + % } + + %= form_for 'set_token' => begin + %= csrf_field + %= hidden_field 'token' => 'status' + + + %= end +
History + % if ($token->{history}) { + %= $acc->{id} . '-' . $token->{history} + % } + % else { + — + % } + + %= form_for 'set_token' => begin + %= csrf_field + %= hidden_field 'token' => 'history' + + + %= end +
Travel + % if ($token->{action}) { + %= $acc->{id} . '-' . $token->{action} + % } + % else { + — + % } + + %= form_for 'set_token' => begin + %= csrf_field + %= hidden_field 'token' => 'action' + + + %= end +
+
+
+ +

Status

+% my $api_root = $self->url_for('/api/v0')->to_abs->scheme('https'); +
+
+

+ Das Format der API v0 kann sich noch ändern, ab v1 ist es stabil. +

+

+ % if ($token->{status}) { + curl <%= $api_root %>/status/<%= $acc->{id} %>-<%= $token->{status} // 'TOKEN' %> + % } + % else { + curl <%= $api_root %>/status/TOKEN + % } +

+

+ {
+ "checked_in" : true / false,
+ "station_ds100" : "EE", (DS100-Kürzel der letzten Station)
+ "station_name" : "Essen Hbf", (Name der letzten Station)
+ "train_type" : "ICE", (aktueller / letzter Zugtyp)
+ "train_line" : "", (Linie, ggf. null)
+ "train_no" : "1234", (Zugnummer)
+ "action_ts" : 1234567, (UNIX-Timestamp des letzten Checkin/Checkout)
+ "sched_ts" : 1234567, (UNIX-Timestamp der zugehörigen Ankunft/Abfahrt gemäß Fahrplan. Ggf. 0)
+ "real_ts" : 1234567, (UNIX-Timestamp der zugehörigen Ankunft/Abfahrt laut Echtzeitdaten. Ggf. 0)
+ } +

+

+ Im Fehlerfall: { "error" : "Begründung" } +

+
+
+

Export