diff options
| -rwxr-xr-x | index.pl | 170 | ||||
| -rwxr-xr-x | migrate.pl | 21 | ||||
| -rw-r--r-- | templates/account.html.ep | 119 | 
3 files changed, 303 insertions, 7 deletions
| @@ -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'); @@ -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"> | 
