Loading lib/Travelynx.pm +113 −0 Original line number Diff line number Diff line Loading @@ -347,6 +347,7 @@ sub startup { "Checkin($uid): INSERT failed: $@"); return ( undef, 'INSERT failed: ' . $@ ); } $self->run_hook( $self->current_user->{id}, 'checkin' ); return ( $train, undef ); } } Loading @@ -366,6 +367,7 @@ sub startup { $self->app->log->error("Undo($uid, $journey_id): $@"); return "Undo($journey_id): $@"; } $self->run_hook( $uid, 'undo' ); return undef; } if ( $journey_id !~ m{ ^ \d+ $ }x ) { Loading Loading @@ -421,6 +423,7 @@ sub startup { $self->app->log->error("Undo($uid, $journey_id): $@"); return "Undo($journey_id): $@"; } $self->run_hook( $uid, 'undo' ); return undef; } ); Loading Loading @@ -572,6 +575,7 @@ sub startup { if ( $has_arrived or $force ) { return ( 0, undef ); $self->run_hook( $uid, 'checkout' ); } return ( 1, undef ); } Loading Loading @@ -984,6 +988,113 @@ sub startup { } ); $self->helper( 'get_webhook' => sub { my ( $self, $uid ) = @_; $uid //= $self->current_user->{id}; my $res_h = $self->pg->db->select( 'webhooks_str', '*', { user_id => $uid } )->hash; $res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} ); return $res_h; } ); $self->helper( 'set_webhook' => sub { my ( $self, %opt ) = @_; $opt{uid} //= $self->current_user->{id}; my $res = $self->pg->db->insert( 'webhooks', { user_id => $opt{uid}, enabled => $opt{enabled}, url => $opt{url}, token => $opt{token} }, { on_conflict => \ '(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null' } ); } ); $self->helper( 'mark_hook_status' => sub { my ( $self, $uid, $url, $success, $text ) = @_; if ( length($text) > 1024 ) { $text = "(output too long)"; } $self->pg->db->update( 'webhooks', { errored => !$success, latest_run => DateTime->now( time_zone => 'Europe/Berlin' ), output => $text, }, { user_id => $uid, url => $url } ); } ); $self->helper( 'run_hook' => sub { my ( $self, $uid, $reason ) = @_; my $hook = $self->get_webhook($uid); if ( not $hook->{enabled} or not $hook->{url} =~ m{^ https?:// }x ) { return; } my $status = { todo => 1 }; my $header = {}; my $hook_body = { reason => $reason, status => $status, }; if ( $hook->{token} ) { $hook->{token} =~ tr{\r\n}{}d; $header->{Authorization} = "Bearer $hook->{token}"; } my $ua = $self->ua; $ua->request_timeout(10); $ua->post_p( $hook->{url} => $header => json => $hook_body )->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { $self->mark_hook_status( $uid, $hook->{url}, 0, "HTTP $err->{code} $err->{message}" ); } else { $self->mark_hook_status( $uid, $hook->{url}, 1, $tx->result->body ); } } )->catch( sub { my ($err) = @_; $self->mark_hook_status( $uid, $hook->{url}, 0, $err ); } )->wait; } ); $self->helper( 'get_user_password' => sub { my ( $self, $name ) = @_; Loading Loading @@ -1753,6 +1864,7 @@ sub startup { $authed_r->get('/account')->to('account#account'); $authed_r->get('/account/privacy')->to('account#privacy'); $authed_r->get('/account/hooks')->to('account#webhook'); $authed_r->get('/ajax/status_card.html')->to('traveling#status_card'); $authed_r->get('/cancelled')->to('traveling#cancelled'); $authed_r->get('/account/password')->to('account#password_form'); Loading @@ -1767,6 +1879,7 @@ sub startup { $authed_r->get('/s/*station')->to('traveling#station'); $authed_r->get('/confirm_mail/:token')->to('account#confirm_mail'); $authed_r->post('/account/privacy')->to('account#privacy'); $authed_r->post('/account/hooks')->to('account#webhook'); $authed_r->post('/journey/add')->to('traveling#add_journey_form'); $authed_r->post('/journey/edit')->to('traveling#edit_journey'); $authed_r->post('/account/password')->to('account#change_password'); Loading lib/Travelynx/Command/database.pm +25 −0 Original line number Diff line number Diff line Loading @@ -456,6 +456,31 @@ my @migrations = ( } ); }, # v10 -> v11 sub { my ($db) = @_; $db->query( qq{ create table webhooks ( user_id integer not null references users (id) primary key, enabled boolean not null, url varchar(1000) not null, token varchar(250), errored boolean, latest_run timestamptz, output text ); comment on table webhooks is 'URLs and bearer tokens for push events'; create view webhooks_str as select user_id, enabled, url, token, errored, output, extract(epoch from latest_run) as latest_run_ts from webhooks ; update schema_version set version = 11; } ); }, ); sub setup_db { Loading lib/Travelynx/Controller/Account.pm +25 −0 Original line number Diff line number Diff line Loading @@ -230,6 +230,31 @@ sub privacy { } } sub webhook { my ($self) = @_; my $hook = $self->get_webhook; if ( $self->param('action') and $self->param('action') eq 'save' ) { $hook->{url} = $self->param('url'); $hook->{token} = $self->param('token'); $hook->{enabled} = $self->param('enabled') // 0; $self->set_webhook( url => $hook->{url}, token => $hook->{token}, enabled => $hook->{enabled} ); $hook = $self->get_webhook; } else { $self->param( url => $hook->{url} ); $self->param( token => $hook->{token} ); $self->param( enabled => $hook->{enabled} ); } $self->render( 'webhooks', hook => $hook ); } sub change_mail { my ($self) = @_; Loading templates/webhooks.html.ep 0 → 100644 +62 −0 Original line number Diff line number Diff line % if (my $invalid = stash('invalid')) { %= include '_invalid_input', invalid => $invalid % } <h1>Web Hooks</h1> <!-- -H "Authorization: Bearer ${TOKEN}" --> <div class="row"> <div class="col s12"> <p> Die im Web Hook konfigurierte URL wird bei jedem Checkin und Checkout des ausgewählten Zuges aufgerufen. Falls ein Token eingetragen ist, wird er als Bearer Token verwendet. </p> <p> Events werden als JSON POST übertragen. Das JSON-Dokument besteht aus zwei Feldern: „reason“ gibt den Grund des API-Aufrufs an (checkin, checkout, undo), „status“ den <a href="/api">aktuellen Status</a>. </p> </div> %= form_for '/account/hooks' => (method => 'POST') => begin %= csrf_field <div class="col s12 center-align"> <label> %= check_box enabled => 1 <span>Aktiv</span> </label> </div> <div class="input-field col s12"> <i class="material-icons prefix">link</i> %= text_field 'url', id => 'url', class => 'validate', maxlength => 1000 <label for="url">URL</label> </div> <div class="input-field col s12"> <i class="material-icons prefix">lock</i> %= text_field 'token', id => 'token', class => 'validate', maxlength => 250 <label for="token">Token</label> </div> <div class="col s12"> % if ($hook->{latest_run}->epoch) { Zuletzt ausgeführt: <%= $hook->{latest_run} %><br/> % if ($hook->{errored}) { <i class="material-icons left">error</i> Status: <%= $hook->{output} %> % } % else { <i class="material-icons left">check</i> Server-Antwort: <%= $hook->{output} %> % } % } % else { Noch nicht ausgeführt. % } </div> <div class="col s12 center-align"> <button class="btn waves-effect waves-light" type="submit" name="action" value="save"> Speichern <i class="material-icons right">send</i> </button> </div> %= end </div> Loading
lib/Travelynx.pm +113 −0 Original line number Diff line number Diff line Loading @@ -347,6 +347,7 @@ sub startup { "Checkin($uid): INSERT failed: $@"); return ( undef, 'INSERT failed: ' . $@ ); } $self->run_hook( $self->current_user->{id}, 'checkin' ); return ( $train, undef ); } } Loading @@ -366,6 +367,7 @@ sub startup { $self->app->log->error("Undo($uid, $journey_id): $@"); return "Undo($journey_id): $@"; } $self->run_hook( $uid, 'undo' ); return undef; } if ( $journey_id !~ m{ ^ \d+ $ }x ) { Loading Loading @@ -421,6 +423,7 @@ sub startup { $self->app->log->error("Undo($uid, $journey_id): $@"); return "Undo($journey_id): $@"; } $self->run_hook( $uid, 'undo' ); return undef; } ); Loading Loading @@ -572,6 +575,7 @@ sub startup { if ( $has_arrived or $force ) { return ( 0, undef ); $self->run_hook( $uid, 'checkout' ); } return ( 1, undef ); } Loading Loading @@ -984,6 +988,113 @@ sub startup { } ); $self->helper( 'get_webhook' => sub { my ( $self, $uid ) = @_; $uid //= $self->current_user->{id}; my $res_h = $self->pg->db->select( 'webhooks_str', '*', { user_id => $uid } )->hash; $res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} ); return $res_h; } ); $self->helper( 'set_webhook' => sub { my ( $self, %opt ) = @_; $opt{uid} //= $self->current_user->{id}; my $res = $self->pg->db->insert( 'webhooks', { user_id => $opt{uid}, enabled => $opt{enabled}, url => $opt{url}, token => $opt{token} }, { on_conflict => \ '(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null' } ); } ); $self->helper( 'mark_hook_status' => sub { my ( $self, $uid, $url, $success, $text ) = @_; if ( length($text) > 1024 ) { $text = "(output too long)"; } $self->pg->db->update( 'webhooks', { errored => !$success, latest_run => DateTime->now( time_zone => 'Europe/Berlin' ), output => $text, }, { user_id => $uid, url => $url } ); } ); $self->helper( 'run_hook' => sub { my ( $self, $uid, $reason ) = @_; my $hook = $self->get_webhook($uid); if ( not $hook->{enabled} or not $hook->{url} =~ m{^ https?:// }x ) { return; } my $status = { todo => 1 }; my $header = {}; my $hook_body = { reason => $reason, status => $status, }; if ( $hook->{token} ) { $hook->{token} =~ tr{\r\n}{}d; $header->{Authorization} = "Bearer $hook->{token}"; } my $ua = $self->ua; $ua->request_timeout(10); $ua->post_p( $hook->{url} => $header => json => $hook_body )->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { $self->mark_hook_status( $uid, $hook->{url}, 0, "HTTP $err->{code} $err->{message}" ); } else { $self->mark_hook_status( $uid, $hook->{url}, 1, $tx->result->body ); } } )->catch( sub { my ($err) = @_; $self->mark_hook_status( $uid, $hook->{url}, 0, $err ); } )->wait; } ); $self->helper( 'get_user_password' => sub { my ( $self, $name ) = @_; Loading Loading @@ -1753,6 +1864,7 @@ sub startup { $authed_r->get('/account')->to('account#account'); $authed_r->get('/account/privacy')->to('account#privacy'); $authed_r->get('/account/hooks')->to('account#webhook'); $authed_r->get('/ajax/status_card.html')->to('traveling#status_card'); $authed_r->get('/cancelled')->to('traveling#cancelled'); $authed_r->get('/account/password')->to('account#password_form'); Loading @@ -1767,6 +1879,7 @@ sub startup { $authed_r->get('/s/*station')->to('traveling#station'); $authed_r->get('/confirm_mail/:token')->to('account#confirm_mail'); $authed_r->post('/account/privacy')->to('account#privacy'); $authed_r->post('/account/hooks')->to('account#webhook'); $authed_r->post('/journey/add')->to('traveling#add_journey_form'); $authed_r->post('/journey/edit')->to('traveling#edit_journey'); $authed_r->post('/account/password')->to('account#change_password'); Loading
lib/Travelynx/Command/database.pm +25 −0 Original line number Diff line number Diff line Loading @@ -456,6 +456,31 @@ my @migrations = ( } ); }, # v10 -> v11 sub { my ($db) = @_; $db->query( qq{ create table webhooks ( user_id integer not null references users (id) primary key, enabled boolean not null, url varchar(1000) not null, token varchar(250), errored boolean, latest_run timestamptz, output text ); comment on table webhooks is 'URLs and bearer tokens for push events'; create view webhooks_str as select user_id, enabled, url, token, errored, output, extract(epoch from latest_run) as latest_run_ts from webhooks ; update schema_version set version = 11; } ); }, ); sub setup_db { Loading
lib/Travelynx/Controller/Account.pm +25 −0 Original line number Diff line number Diff line Loading @@ -230,6 +230,31 @@ sub privacy { } } sub webhook { my ($self) = @_; my $hook = $self->get_webhook; if ( $self->param('action') and $self->param('action') eq 'save' ) { $hook->{url} = $self->param('url'); $hook->{token} = $self->param('token'); $hook->{enabled} = $self->param('enabled') // 0; $self->set_webhook( url => $hook->{url}, token => $hook->{token}, enabled => $hook->{enabled} ); $hook = $self->get_webhook; } else { $self->param( url => $hook->{url} ); $self->param( token => $hook->{token} ); $self->param( enabled => $hook->{enabled} ); } $self->render( 'webhooks', hook => $hook ); } sub change_mail { my ($self) = @_; Loading
templates/webhooks.html.ep 0 → 100644 +62 −0 Original line number Diff line number Diff line % if (my $invalid = stash('invalid')) { %= include '_invalid_input', invalid => $invalid % } <h1>Web Hooks</h1> <!-- -H "Authorization: Bearer ${TOKEN}" --> <div class="row"> <div class="col s12"> <p> Die im Web Hook konfigurierte URL wird bei jedem Checkin und Checkout des ausgewählten Zuges aufgerufen. Falls ein Token eingetragen ist, wird er als Bearer Token verwendet. </p> <p> Events werden als JSON POST übertragen. Das JSON-Dokument besteht aus zwei Feldern: „reason“ gibt den Grund des API-Aufrufs an (checkin, checkout, undo), „status“ den <a href="/api">aktuellen Status</a>. </p> </div> %= form_for '/account/hooks' => (method => 'POST') => begin %= csrf_field <div class="col s12 center-align"> <label> %= check_box enabled => 1 <span>Aktiv</span> </label> </div> <div class="input-field col s12"> <i class="material-icons prefix">link</i> %= text_field 'url', id => 'url', class => 'validate', maxlength => 1000 <label for="url">URL</label> </div> <div class="input-field col s12"> <i class="material-icons prefix">lock</i> %= text_field 'token', id => 'token', class => 'validate', maxlength => 250 <label for="token">Token</label> </div> <div class="col s12"> % if ($hook->{latest_run}->epoch) { Zuletzt ausgeführt: <%= $hook->{latest_run} %><br/> % if ($hook->{errored}) { <i class="material-icons left">error</i> Status: <%= $hook->{output} %> % } % else { <i class="material-icons left">check</i> Server-Antwort: <%= $hook->{output} %> % } % } % else { Noch nicht ausgeführt. % } </div> <div class="col s12 center-align"> <button class="btn waves-effect waves-light" type="submit" name="action" value="save"> Speichern <i class="material-icons right">send</i> </button> </div> %= end </div>