summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cpanfile1
-rw-r--r--cpanfile.snapshot9
-rw-r--r--examples/travelynx.conf27
-rwxr-xr-xlib/Travelynx.pm22
-rw-r--r--lib/Travelynx/Command/database.pm19
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm90
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm78
-rw-r--r--lib/Travelynx/Model/Traewelling.pm7
-rw-r--r--templates/account.html.ep50
-rw-r--r--templates/traewelling.html.ep46
10 files changed, 179 insertions, 170 deletions
diff --git a/cpanfile b/cpanfile
index 8f72042..6bdba37 100644
--- a/cpanfile
+++ b/cpanfile
@@ -10,6 +10,7 @@ requires 'List::UtilsBy';
requires 'MIME::Entity';
requires 'Mojolicious';
requires 'Mojolicious::Plugin::Authentication';
+requires 'Mojolicious::Plugin::OAuth2';
requires 'Mojo::Pg';
requires 'Text::CSV';
requires 'Text::Markdown';
diff --git a/cpanfile.snapshot b/cpanfile.snapshot
index 198917d..5619f56 100644
--- a/cpanfile.snapshot
+++ b/cpanfile.snapshot
@@ -1915,6 +1915,15 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
Mojolicious 8.0
perl 5.016
+ Mojolicious-Plugin-OAuth2-2.02
+ pathname: J/JH/JHTHORSEN/Mojolicious-Plugin-OAuth2-2.02.tar.gz
+ provides:
+ Mojolicious::Plugin::OAuth2 2.02
+ Mojolicious::Plugin::OAuth2::Mock undef
+ requirements:
+ ExtUtils::MakeMaker 0
+ IO::Socket::SSL 1.94
+ Mojolicious 8.25
Moo-2.005005
pathname: H/HA/HAARG/Moo-2.005005.tar.gz
provides:
diff --git a/examples/travelynx.conf b/examples/travelynx.conf
index c77e40f..f8eaac0 100644
--- a/examples/travelynx.conf
+++ b/examples/travelynx.conf
@@ -97,7 +97,31 @@
die("Changeme!"),
],
+ # optionally, users can link travelynx and traewelling accounts, and
+ # automatically synchronize check-ins.
+ # To do so, you need to create a travelynx application on
+ # <https://traewelling.de/settings/applications>. The application
+ # must be marked as "Confidential" and have a redirect URL that matches
+ # $base_url/oauth/traewelling, where $base_url refers to the URL configured
+ # above. For instance, travelynx.de uses
+ # 'https://travelynx.de/oauth/traewelling'. An incorrect redirect URL will
+ # cause OAuth2 to fail with unsupported_grant_type.
+ #
+ # Note that the travelynx/traewelling OAuth2 integration does not support
+ # travelynx installations that are reachable on multiple URLs at the
+ # moment -- linking a traewelling account is only possible when accessing
+ # travelynx via the base URL.
traewelling => {
+
+ # Uncomment the following block and insert the application ID and
+ # secret obtained from https://traewelling.de/settings/applications
+ # -> your application -> Edit.
+
+ #oauth => {
+ # id => 1234,
+ # secret => 'mysecret',
+ #}
+
# By default, the "work" or "worker" command does not just update
# real-time data of active journeys, but also performs push and pull
# synchronization with traewelling for accounts that have configured it.
@@ -110,7 +134,8 @@
# periodically runs "perl index.pl traewelling" (push and pull) or
# two separate cronjobs that run "perl index.pl traewelling push" and
# "perl index.pl traewelling pull", respectively.
- ## separate_worker => 1,
+
+ # separate_worker => 1,
},
version => qx{git describe --dirty} // 'experimental',
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index f5f56b7..551c061 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -100,6 +100,23 @@ sub startup {
},
}
);
+
+ if ( my $oa = $self->config->{traewelling}{oauth} ) {
+ $self->plugin(
+ OAuth2 => {
+ providers => {
+ traewelling => {
+ key => $oa->{id},
+ secret => $oa->{secret},
+ authorize_url =>
+'https://traewelling.de/oauth/authorize?response_type=code',
+ token_url => 'https://traewelling.de/oauth/token',
+ }
+ }
+ }
+ );
+ }
+
$self->sessions->default_expiration( 60 * 60 * 24 * 180 );
# Starting with v8.11, Mojolicious sends SameSite=Lax Cookies by default.
@@ -2140,6 +2157,11 @@ sub startup {
$r->post('/login')->to('account#do_login');
$r->post('/recover')->to('account#request_password_reset');
+ if ( $self->config->{traewelling}{oauth} ) {
+ $r->get('/oauth/traewelling')->to('traewelling#oauth');
+ $r->post('/oauth/traewelling')->to('traewelling#oauth');
+ }
+
if ( not $self->config->{registration}{disabled} ) {
$r->get('/register')->to('account#registration_form');
$r->post('/register')->to('account#register');
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index 10732ec..b45a02f 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -1815,6 +1815,25 @@ my @migrations = (
}
);
},
+
+ # v45 -> v46
+ # Switch to Traewelling OAuth2 authentication.
+ # E-Mail is no longer needed.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view traewelling_str;
+ create view traewelling_str as select
+ user_id, push_sync, pull_sync, errored, token, data,
+ extract(epoch from latest_run) as latest_run_ts
+ from traewelling
+ ;
+ alter table traewelling drop column email;
+ update schema_version set version = 46;
+ }
+ );
+ },
);
# TODO add 'hafas' column to in_transit (and maybe journeys? undo/redo needs something to work with...)
diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm
index 4c6bc64..e14872d 100644
--- a/lib/Travelynx/Controller/Traewelling.pm
+++ b/lib/Travelynx/Controller/Traewelling.pm
@@ -6,11 +6,9 @@ package Travelynx::Controller::Traewelling;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Promise;
-sub settings {
+sub oauth {
my ($self) = @_;
- my $uid = $self->current_user->{id};
-
if ( $self->param('action')
and $self->validation->csrf_protect->has_error('csrf_token') )
{
@@ -22,38 +20,68 @@ sub settings {
return;
}
- if ( $self->param('action') and $self->param('action') eq 'login' ) {
- my $email = $self->param('email');
- my $password = $self->param('password');
- $self->render_later;
- $self->traewelling_api->login_p(
- uid => $uid,
- email => $email,
- password => $password
- )->then(
- sub {
- my $traewelling = $self->traewelling->get( uid => $uid );
- $self->param( sync_source => 'none' );
- $self->render(
- 'traewelling',
- traewelling => $traewelling,
- new_traewelling => 1,
- );
+ $self->render_later;
+
+ my $oa = $self->config->{traewelling}{oauth};
+
+ return $self->oauth2->get_token_p(
+ traewelling => { scope => 'read-statuses write-statuses' } )->then(
+ sub {
+ my ($provider) = @_;
+ if ( not defined $provider ) {
+
+ # OAuth2 plugin performed a redirect, no need to render
+ return;
}
- )->catch(
- sub {
- my ($err) = @_;
- $self->render(
- 'traewelling',
- traewelling => {},
- new_traewelling => 1,
- login_error => $err,
- );
+ if ( not $provider or not $provider->{access_token} ) {
+ $self->flash( new_traewelling => 1 );
+ $self->flash( login_error => 'no token received' );
+ $self->redirect_to('/account/traewelling');
+ return;
}
- )->wait;
+ my $uid = $self->current_user->{id};
+ my $token = $provider->{access_token};
+ $self->traewelling->link(
+ uid => $self->current_user->{id},
+ token => $provider->{access_token},
+ expires_in => $provider->{expires_in},
+ );
+ return $self->traewelling_api->get_user_p( $uid, $token )->then(
+ sub {
+ $self->flash( new_traewelling => 1 );
+ $self->redirect_to('/account/traewelling');
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ say "error $err";
+ $self->flash( new_traewelling => 1 );
+ $self->flash( login_error => $err );
+ $self->redirect_to('/account/traewelling');
+ return;
+ }
+ );
+}
+
+sub settings {
+ my ($self) = @_;
+
+ my $uid = $self->current_user->{id};
+
+ if ( $self->param('action')
+ and $self->validation->csrf_protect->has_error('csrf_token') )
+ {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
- elsif ( $self->param('action') and $self->param('action') eq 'logout' ) {
+
+ if ( $self->param('action') and $self->param('action') eq 'logout' ) {
$self->render_later;
my $traewelling = $self->traewelling->get( uid => $uid );
$self->traewelling_api->logout_p(
diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm
index 23170eb..18edc18 100644
--- a/lib/Travelynx/Helper/Traewelling.pm
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -199,84 +199,6 @@ sub get_user_p {
return $promise;
}
-sub login_p {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid};
- my $email = $opt{email};
- my $password = $opt{password};
-
- my $ua = $self->{user_agent}->request_timeout(20);
-
- my $request = {
- login => $email,
- password => $password,
- };
-
- my $promise = Mojo::Promise->new;
- my $token;
-
- $ua->post_p(
- "https://traewelling.de/api/v1/auth/login" => $self->{header},
- json => $request
- )->then(
- sub {
- my ($tx) = @_;
- if ( my $err = $tx->error ) {
- my $err_msg
- = "v1/auth/login: HTTP $err->{code} $err->{message}";
- $promise->reject($err_msg);
- return;
- }
- else {
- my $res = $tx->result->json->{data};
- $token = $res->{token};
- my $expiry_dt = $self->parse_datetime( $res->{expires_at} );
-
- # Fall back to one year expiry
- $expiry_dt //= DateTime->now( time_zone => 'Europe/Berlin' )
- ->add( years => 1 );
- $self->{model}->link(
- uid => $uid,
- email => $email,
- token => $token,
- expires => $expiry_dt
- );
- return $self->get_user_p( $uid, $token );
- }
- }
- )->then(
- sub {
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- if ($token) {
-
- # We have a token, but couldn't complete the login. For now, we
- # solve this by logging out and invalidating the token.
- $self->logout_p(
- uid => $uid,
- token => $token
- )->finally(
- sub {
- $promise->reject("v1/auth/login: $err");
- return;
- }
- );
- }
- else {
- $promise->reject("v1/auth/login: $err");
- }
- return;
- }
- )->wait;
-
- return $promise;
-}
-
sub logout_p {
my ( $self, %opt ) = @_;
diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm
index 1939374..72ee92d 100644
--- a/lib/Travelynx/Model/Traewelling.pm
+++ b/lib/Travelynx/Model/Traewelling.pm
@@ -38,16 +38,15 @@ sub now {
sub link {
my ( $self, %opt ) = @_;
- my $log = [ [ $self->now->epoch, "Erfolgreich angemeldet" ] ];
+ my $log = [ [ $self->now->epoch, "Erfolgreich mittels OAuth2 verbunden" ] ];
my $data = {
log => $log,
- expires => $opt{expires}->epoch,
+ expires => $self->now->epoch + $opt{expires_in},
};
my $user_entry = {
user_id => $opt{uid},
- email => $opt{email},
push_sync => 0,
pull_sync => 0,
token => $opt{token},
@@ -59,7 +58,7 @@ sub link {
$user_entry,
{
on_conflict => \
-'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, push_sync = false, pull_sync = false, data = null, errored = false, latest_run = null'
+'(user_id) do update set token = EXCLUDED.token, push_sync = false, pull_sync = false, data = null, errored = false, latest_run = null'
}
);
diff --git a/templates/account.html.ep b/templates/account.html.ep
index c27e0f5..b64869a 100644
--- a/templates/account.html.ep
+++ b/templates/account.html.ep
@@ -122,33 +122,35 @@
% }
</td>
</tr>
- <tr>
- <th scope="row">Träwelling</th>
- <td>
- <a href="/account/traewelling"><i class="material-icons">edit</i></a>
- % if (not ($traewelling->{token})) {
- <span style="color: #999999;">Nicht verknüpft</span>
- % }
- % elsif ($traewelling->{errored}) {
- Fehlerhaft <i class="material-icons" aria-hidden="true">error</i>
- % }
- % else {
- Verknüpft mit <%= $traewelling->{data}{user_name} // $traewelling->{email} %>
- % if ($traewelling->{expired}) {
- – Login-Token abgelaufen <i class="material-icons" aria-hidden="true">error</i>
- % }
- % elsif ($traewelling->{expiring}) {
- – Login-Token läuft bald ab <i class="material-icons" aria-hidden="true">warning</i>
+ % if (config->{traewelling}{oauth}) {
+ <tr>
+ <th scope="row">Träwelling</th>
+ <td>
+ <a href="/account/traewelling"><i class="material-icons">edit</i></a>
+ % if (not ($traewelling->{token})) {
+ <span style="color: #999999;">Nicht verknüpft</span>
% }
- % elsif ($traewelling->{pull_sync}) {
- – Checkins in Träwelling werden von travelynx übernommen
+ % elsif ($traewelling->{errored}) {
+ Fehlerhaft <i class="material-icons" aria-hidden="true">error</i>
% }
- % elsif ($traewelling->{push_sync}) {
- – Checkins in travelynx werden zu Träwelling weitergereicht
+ % else {
+ Verknüpft mit <%= $traewelling->{data}{user_name} // $traewelling->{email} %>
+ % if ($traewelling->{expired}) {
+ – Login-Token abgelaufen <i class="material-icons" aria-hidden="true">error</i>
+ % }
+ % elsif ($traewelling->{expiring}) {
+ – Login-Token läuft bald ab <i class="material-icons" aria-hidden="true">warning</i>
+ % }
+ % elsif ($traewelling->{pull_sync}) {
+ – Checkins in Träwelling werden von travelynx übernommen
+ % }
+ % elsif ($traewelling->{push_sync}) {
+ – Checkins in travelynx werden zu Träwelling weitergereicht
+ % }
% }
- % }
- </td>
- </tr>
+ </td>
+ </tr>
+ % }
<tr>
<th scope="row">Externe Dienste</th>
<td>
diff --git a/templates/traewelling.html.ep b/templates/traewelling.html.ep
index 23e2e35..4147140 100644
--- a/templates/traewelling.html.ep
+++ b/templates/traewelling.html.ep
@@ -4,20 +4,20 @@
<h1>Träwelling</h1>
-% if (stash('new_traewelling')) {
+% if (flash('new_traewelling')) {
<div class="row">
<div class="col s12">
% if ($traewelling->{token}) {
<div class="card success-color">
<div class="card-content white-text">
<span class="card-title">Träwelling verknüpft</span>
- % my $user = $traewelling->{data}{user_name} // $traewelling->{email};
+ % my $user = $traewelling->{data}{user_name} // '???';
<p>Dein travelynx-Account hat nun ein Jahr lang Zugriff auf
den Träwelling-Account <b>@<%= $user %></b>.</p>
</div>
</div>
% }
- % elsif (my $login_err = stash('login_error')) {
+ % elsif (my $login_err = flash('login_error')) {
<div class="card caution-color">
<div class="card-content white-text">
<span class="card-title">Login-Fehler</span>
@@ -30,7 +30,7 @@
<div class="card-content white-text">
<span class="card-title">Logout-Fehler</span>
<p>Der Logout bei Träwelling ist fehlgeschlagen: <%= $logout_err %>.
- Dein Login-Token bei travelynx wurde dennoch gelöscht, so
+ Dein Token bei travelynx wurde dennoch gelöscht, so
dass nun kein Zugriff von travelynx auf Träwelling mehr
möglich ist. In den <a
href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
@@ -73,10 +73,10 @@
<div class="card caution-color">
<div class="card-content white-text">
% if ($traewelling->{expired}) {
- <span class="card-title">Login-Token abgelaufen</span>
+ <span class="card-title">Token abgelaufen</span>
% }
% else {
- <span class="card-title">Login-Token läuft bald ab</span>
+ <span class="card-title">Token läuft bald ab</span>
% }
<p>Melde deinen travelynx-Account von Träwelling ab und
verbinde ihn mit deinem Träwelling-Passwort erneut,
@@ -105,37 +105,19 @@
verknüpfen. Dies erlaubt die automatische Übernahme zukünftiger
Checkins zwischen den beiden Diensten. Träwelling-Checkins in
Nahverkehrsmittel und Züge außerhalb des deutschen Schienennetzes
- werden nicht unterstützt und ignoriert. Checkins, die vor dem
- Verknüpfen der Accounts stattgefunden haben, werden nicht
- synchronisiert. Bei synchronisierten Checkins wird der zugehörige
- Träwelling-Status von deiner travelynx-Statusseite aus verlinkt.
- </p>
- <p>
- Mit E-Mail und Passwort wird ein Login über die Träwelling-API
- durchgeführt. Die E-Mail und das dabei generierte Token werden
- von travelynx gespeichert. Das Passwort wird ausschließlich für
- den Login verwendet und nicht gespeichert. Der Login kann jederzeit
- sowohl auf dieser Seite als auch über die <a
- href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
- widerrufen werden. Nach einem Jahr läuft er automatisch ab.
+ werden (noch) nicht unterstützt und ignoriert. Checkins, die
+ vor dem Verknüpfen der Accounts stattgefunden haben, werden
+ nicht synchronisiert. Bei synchronisierten Checkins wird der
+ zugehörige Träwelling-Status von deiner travelynx-Statusseite
+ aus verlinkt.
</p>
</div>
</div>
<div class="row">
- %= form_for '/account/traewelling' => (method => 'POST') => begin
+ %= form_for '/oauth/traewelling' => (method => 'POST') => begin
%= csrf_field
- <div class="input-field col s12">
- <i class="material-icons prefix">account_circle</i>
- %= text_field 'email', id => 'email', class => 'validate', required => undef, maxlength => 250
- <label for="email">Login (Name oder E-Mail)</label>
- </div>
- <div class="input-field col s12">
- <i class="material-icons prefix">lock</i>
- %= password_field 'password', id => 'password', class => 'validate', required => undef
- <label for="password">Passwort</label>
- </div>
<div class="col s12 center-align">
- <button class="btn waves-effect waves-light" type="submit" name="action" value="login">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="connect">
Verknüpfen
<i class="material-icons right">send</i>
</button>
@@ -154,7 +136,7 @@
% else {
%= $traewelling->{email}
% }
- verknüpft. Der Login-Token läuft <%= $traewelling->{expires_on}->strftime('am %d.%m.%Y um %H:%M Uhr') %> ab.
+ verknüpft. Der Token läuft <%= $traewelling->{expires_on}->strftime('am %d.%m.%Y um %H:%M Uhr') %> ab.
</p>
</div>
</div>