summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xindex.pl170
-rwxr-xr-xmigrate.pl21
-rw-r--r--templates/account.html.ep119
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 => {
@@ -275,6 +280,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{ ^ (?<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');
@@ -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 @@
</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">