travelynx/lib/Travelynx/Controller/Api.pm

608 lines
12 KiB
Perl
Raw Normal View History

package Travelynx::Controller::Api;
2023-02-19 08:35:38 +00:00
# Copyright (C) 2020-2023 Daniel Friesel
2020-11-27 21:12:56 +00:00
#
2021-01-29 17:32:13 +00:00
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
2019-12-14 19:46:02 +00:00
use DateTime;
2019-12-14 20:53:23 +00:00
use List::Util;
use UUID::Tiny qw(:std);
# Internal Helpers
sub make_token {
return create_uuid_as_string(UUID_V4);
}
2019-12-14 19:46:02 +00:00
sub sanitize {
my ( $type, $value ) = @_;
if ( not defined $value ) {
return undef;
}
if ( $type eq '' ) {
return '' . $value;
}
2020-01-26 09:48:41 +00:00
if ( $value =~ m{ ^ [0-9.e]+ $ }x ) {
return 0 + $value;
}
return 0;
2019-12-14 19:46:02 +00:00
}
# Contollers
2019-05-02 09:29:43 +00:00
sub documentation {
my ($self) = @_;
if ( $self->is_user_authenticated ) {
2023-01-22 10:34:53 +00:00
my $uid = $self->current_user->{id};
$self->render(
'api_documentation',
2023-01-22 10:34:53 +00:00
uid => $uid,
api_token => $self->users->get_api_token( uid => $uid ),
);
}
else {
$self->render('api_documentation');
}
2019-05-02 09:29:43 +00:00
}
2019-04-24 05:34:41 +00:00
sub get_v1 {
my ($self) = @_;
my $api_action = $self->stash('user_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};
if ( $uid > 2147483647 ) {
$self->render(
json => {
error => 'Malformed token',
},
);
return;
}
2023-01-22 10:34:53 +00:00
my $token = $self->users->get_api_token( uid => $uid );
if ( not $api_token
or not $token->{$api_action}
or $api_token ne $token->{$api_action} )
{
2019-04-24 05:34:41 +00:00
$self->render(
json => {
error => 'Invalid token',
},
);
return;
}
if ( $api_action eq 'status' ) {
$self->render( json => $self->get_user_status_json_v1( uid => $uid ) );
2019-04-24 05:34:41 +00:00
}
else {
$self->render(
json => {
error => 'not implemented',
},
);
}
}
2019-12-14 20:53:23 +00:00
sub travel_v1 {
my ($self) = @_;
my $payload = $self->req->json;
if ( not $payload or ref($payload) ne 'HASH' ) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
error => 'Malformed JSON',
},
);
return;
}
2019-12-14 20:53:23 +00:00
my $api_token = $payload->{token} // '';
if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
error => 'Malformed token',
2019-12-14 20:53:23 +00:00
},
);
return;
}
my $uid = $+{id};
$api_token = $+{token};
if ( $uid > 2147483647 ) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
error => 'Malformed token',
2019-12-14 20:53:23 +00:00
},
);
return;
}
2023-01-22 10:34:53 +00:00
my $token = $self->users->get_api_token( uid => $uid );
if ( not $token->{'travel'} or $api_token ne $token->{'travel'} ) {
2019-12-14 20:53:23 +00:00
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
error => 'Invalid token',
2019-12-14 20:53:23 +00:00
},
);
return;
}
if ( not exists $payload->{action}
or $payload->{action} !~ m{^(checkin|checkout|undo)$} )
{
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
error => 'Missing or invalid action',
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
},
);
return;
}
if ( $payload->{action} eq 'checkin' ) {
my $from_station = sanitize( q{}, $payload->{fromStation} );
my $to_station = sanitize( q{}, $payload->{toStation} );
my $train_id;
if (
not(
$from_station
and ( ( $payload->{train}{type} and $payload->{train}{no} )
or $payload->{train}{id} )
)
)
{
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Missing fromStation or train data',
status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
if ( not $self->stations->search($from_station) ) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Unknown fromStation',
status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
if ( $to_station and not $self->stations->search($to_station) ) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Unknown toStation',
status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
2019-12-14 20:53:23 +00:00
if ( exists $payload->{train}{id} ) {
$train_id = sanitize( 0, $payload->{train}{id} );
}
else {
my $train_type = sanitize( q{}, $payload->{train}{type} );
my $train_no = sanitize( q{}, $payload->{train}{no} );
my $status = $self->iris->get_departures(
station => $from_station,
lookbehind => 140,
lookahead => 40
);
2019-12-14 20:53:23 +00:00
if ( $status->{errstr} ) {
$self->render(
json => {
success => \0,
error =>
2019-12-20 17:33:59 +00:00
'Error requesting departures from fromStation: '
2019-12-14 20:53:23 +00:00
. $status->{errstr},
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
return;
}
my ($train) = List::Util::first {
$_->type eq $train_type and $_->train_no eq $train_no
}
@{ $status->{results} };
if ( not defined $train ) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
2019-12-20 17:33:59 +00:00
error => 'Train not found at fromStation',
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
return;
}
$train_id = $train->train_id;
}
my ( $train, $error ) = $self->checkin(
station => $from_station,
train_id => $train_id,
uid => $uid
);
if ( $payload->{comment} and not $error ) {
$self->in_transit->update_user_data(
uid => $uid,
user_data => { comment => sanitize( q{}, $payload->{comment} ) }
);
2019-12-14 22:46:36 +00:00
}
2019-12-14 20:53:23 +00:00
if ( $to_station and not $error ) {
( $train, $error ) = $self->checkout(
station => $to_station,
force => 0,
uid => $uid
);
2019-12-14 20:53:23 +00:00
}
if ($error) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
2019-12-20 17:33:59 +00:00
error => 'Checkin/Checkout error: ' . $error,
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
}
else {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \1,
deprecated => \0,
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
}
}
elsif ( $payload->{action} eq 'checkout' ) {
my $to_station = sanitize( q{}, $payload->{toStation} );
if ( not $to_station ) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Missing toStation',
status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
2019-12-14 22:46:36 +00:00
if ( $payload->{comment} ) {
$self->in_transit->update_user_data(
uid => $uid,
user_data => { comment => sanitize( q{}, $payload->{comment} ) }
);
2019-12-14 22:46:36 +00:00
}
my ( $train, $error ) = $self->checkout(
station => $to_station,
force => $payload->{force} ? 1 : 0,
uid => $uid
);
2019-12-14 20:53:23 +00:00
if ($error) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
2019-12-20 17:33:59 +00:00
error => 'Checkout error: ' . $error,
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
}
else {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \1,
deprecated => \0,
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
}
}
elsif ( $payload->{action} eq 'undo' ) {
my $error = $self->undo( 'in_transit', $uid );
if ($error) {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \0,
deprecated => \0,
error => $error,
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
}
else {
$self->render(
json => {
2019-12-20 15:23:41 +00:00
success => \1,
deprecated => \0,
status => $self->get_user_status_json_v1( uid => $uid )
2019-12-14 20:53:23 +00:00
}
);
}
}
}
2019-12-14 19:46:02 +00:00
sub import_v1 {
my ($self) = @_;
my $payload = $self->req->json;
if ( not $payload or ref($payload) ne 'HASH' ) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Malformed JSON',
},
);
return;
}
2019-12-14 19:46:02 +00:00
my $api_token = $payload->{token} // '';
if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Malformed token',
2019-12-14 19:46:02 +00:00
},
);
return;
}
my $uid = $+{id};
$api_token = $+{token};
if ( $uid > 2147483647 ) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Malformed token',
2019-12-14 19:46:02 +00:00
},
);
return;
}
my $token = $self->users->get_api_token( uid => $uid );
if ( not $token->{'import'} or $api_token ne $token->{'import'} ) {
2019-12-14 19:46:02 +00:00
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'Invalid token',
2019-12-14 19:46:02 +00:00
},
);
return;
}
if ( not exists $payload->{fromStation}
or not exists $payload->{toStation} )
{
$self->render(
json => {
success => \0,
deprecated => \0,
error => 'missing fromStation or toStation',
2019-12-14 19:46:02 +00:00
},
);
return;
}
my %opt;
eval {
if ( not $payload->{fromStation}{name}
or not $payload->{toStation}{name} )
{
die("Missing fromStation/toStation name\n");
}
if ( not $payload->{train}{type} or not $payload->{train}{no} ) {
die("Missing train data\n");
}
if ( not $payload->{fromStation}{scheduledTime}
or not $payload->{toStation}{scheduledTime} )
{
die("Missing fromStation/toStation scheduledTime\n");
}
2019-12-14 19:46:02 +00:00
%opt = (
uid => $uid,
train_type => sanitize( q{}, $payload->{train}{type} ),
train_no => sanitize( q{}, $payload->{train}{no} ),
train_line => sanitize( q{}, $payload->{train}{line} ),
cancelled => $payload->{cancelled} ? 1 : 0,
dep_station => sanitize( q{}, $payload->{fromStation}{name} ),
arr_station => sanitize( q{}, $payload->{toStation}{name} ),
2019-12-14 19:46:02 +00:00
sched_departure =>
sanitize( 0, $payload->{fromStation}{scheduledTime} ),
rt_departure => sanitize(
0,
$payload->{fromStation}{realTime}
// $payload->{fromStation}{scheduledTime}
),
sched_arrival =>
sanitize( 0, $payload->{toStation}{scheduledTime} ),
rt_arrival => sanitize(
0,
$payload->{toStation}{realTime}
// $payload->{toStation}{scheduledTime}
),
comment => sanitize( q{}, $payload->{comment} ),
lax => $payload->{lax} ? 1 : 0,
2019-12-14 19:46:02 +00:00
);
2020-01-26 09:48:41 +00:00
if ( $payload->{intermediateStops}
and ref( $payload->{intermediateStops} ) eq 'ARRAY' )
{
$opt{route}
2020-01-26 09:48:41 +00:00
= [ map { sanitize( q{}, $_ ) }
@{ $payload->{intermediateStops} } ];
2019-12-14 19:46:02 +00:00
}
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
$opt{$key} = DateTime->from_epoch(
time_zone => 'Europe/Berlin',
epoch => $opt{$key}
);
}
};
if ($@) {
my ($first_line) = split( qr{\n}, $@ );
$self->render(
json => {
success => \0,
deprecated => \0,
error => $first_line
2019-12-14 19:46:02 +00:00
}
);
return;
}
my $db = $self->pg->db;
my $tx = $db->begin;
$opt{db} = $db;
my ( $journey_id, $error ) = $self->journeys->add(%opt);
2019-12-14 19:46:02 +00:00
my $journey;
if ( not $error ) {
$journey = $self->journeys->get_single(
2019-12-14 19:46:02 +00:00
uid => $uid,
db => $db,
journey_id => $journey_id,
verbose => 1
);
2019-12-17 19:01:39 +00:00
$error
= $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 );
2019-12-14 19:46:02 +00:00
}
if ($error) {
$self->render(
json => {
success => \0,
deprecated => \0,
error => $error
2019-12-14 19:46:02 +00:00
}
);
}
elsif ( $payload->{dryRun} ) {
$self->render(
json => {
success => \1,
deprecated => \0,
id => $journey_id,
result => $journey
2019-12-14 19:46:02 +00:00
}
);
}
else {
$self->journey_stats_cache->invalidate(
ts => $opt{rt_departure},
db => $db,
uid => $uid
);
2019-12-14 19:46:02 +00:00
$tx->commit;
$self->render(
json => {
success => \1,
deprecated => \0,
id => $journey_id,
result => $journey
2019-12-14 19:46:02 +00:00
}
);
}
}
sub set_token {
my ($self) = @_;
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render( 'account', invalid => 'csrf' );
return;
}
my $token = make_token();
my $token_id = $self->users->get_token_id( $self->param('token') );
if ( not $token_id ) {
$self->redirect_to('account');
return;
}
if ( $self->param('action') eq 'delete' ) {
$self->pg->db->delete(
'tokens',
{
user_id => $self->current_user->{id},
type => $token_id
}
);
}
else {
$self->pg->db->insert(
'tokens',
{
user_id => $self->current_user->{id},
type => $token_id,
token => $token
},
{
on_conflict => \
'(user_id, type) do update set token = EXCLUDED.token'
},
);
}
$self->redirect_to('account');
}
1;