Add JSON API
This commit is contained in:
parent
532fadabb4
commit
d6264b5ca8
3 changed files with 303 additions and 7 deletions
170
index.pl
170
index.pl
|
@ -37,8 +37,13 @@ my %action_type = (
|
||||||
checkout => 2,
|
checkout => 2,
|
||||||
undo => 3,
|
undo => 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
my @action_types = (qw(checkin checkout undo));
|
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(
|
app->plugin(
|
||||||
authentication => {
|
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(
|
app->attr(
|
||||||
get_password_query => sub {
|
get_password_query => sub {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
@ -635,6 +691,18 @@ helper 'get_user_data' => sub {
|
||||||
return undef;
|
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 {
|
helper 'get_user_password' => sub {
|
||||||
my ( $self, $name ) = @_;
|
my ( $self, $name ) = @_;
|
||||||
my $query = $self->app->get_password_query;
|
my $query = $self->app->get_password_query;
|
||||||
|
@ -770,9 +838,9 @@ helper 'get_user_travels' => sub {
|
||||||
};
|
};
|
||||||
|
|
||||||
helper 'get_user_status' => 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);
|
$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;
|
||||||
|
|
||||||
|
@ -784,7 +852,9 @@ helper 'get_user_status' => sub {
|
||||||
@cols = @{ $rows->[2] };
|
@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 $checkin_station_name = decode( 'UTF-8', $cols[3] );
|
||||||
my @route = split( qr{[|]}, decode( 'UTF-8', $cols[10] // q{} ) );
|
my @route = split( qr{[|]}, decode( 'UTF-8', $cols[10] // q{} ) );
|
||||||
my @route_after;
|
my @route_after;
|
||||||
|
@ -799,8 +869,10 @@ helper 'get_user_status' => sub {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
checked_in => ( $cols[0] == $action_type{checkin} ),
|
checked_in => ( $cols[0] == $action_type{checkin} ),
|
||||||
timestamp => $ts,
|
timestamp => $action_ts,
|
||||||
timestamp_delta => $now->epoch - $ts->epoch,
|
timestamp_delta => $now->epoch - $action_ts->epoch,
|
||||||
|
sched_ts => $sched_ts,
|
||||||
|
real_ts => $real_ts,
|
||||||
station_ds100 => $cols[2],
|
station_ds100 => $cols[2],
|
||||||
station_name => $checkin_station_name,
|
station_name => $checkin_station_name,
|
||||||
train_type => $cols[4],
|
train_type => $cols[4],
|
||||||
|
@ -813,7 +885,9 @@ helper 'get_user_status' => sub {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
checked_in => 0,
|
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 {
|
get '/login' => sub {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
$self->render('login');
|
$self->render('login');
|
||||||
|
@ -1287,6 +1418,31 @@ post '/logout' => sub {
|
||||||
$self->redirect_to('/login');
|
$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 {
|
get '/s/*station' => sub {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
my $station = $self->stash('station');
|
my $station = $self->stash('station');
|
||||||
|
|
21
migrate.pl
21
migrate.pl
|
@ -174,6 +174,27 @@ my @migrations = (
|
||||||
);
|
);
|
||||||
$dbh->commit;
|
$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();
|
my $schema_version = get_schema_version();
|
||||||
|
|
|
@ -33,6 +33,125 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<h1>Export</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
Loading…
Reference in a new issue