Commit b36ba45a authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

WiP webhook support

parent 55581d1f
Loading
Loading
Loading
Loading
+113 −0
Original line number Diff line number Diff line
@@ -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 );
				}
			}
@@ -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 ) {
@@ -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;
		}
	);
@@ -572,6 +575,7 @@ sub startup {

			if ( $has_arrived or $force ) {
				return ( 0, undef );
				$self->run_hook( $uid, 'checkout' );
			}
			return ( 1, undef );
		}
@@ -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 ) = @_;
@@ -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');
@@ -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');
+25 −0
Original line number Diff line number Diff line
@@ -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 {
+25 −0
Original line number Diff line number Diff line
@@ -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) = @_;

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