Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions eg/meet-space/myapp.pl
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
plugin 'Yancy::Plugin::Editor', {
model => $model,
storage => $storage,
# Development app: Allow editing without logging in
allow_anonymous => 1,
};
### /Mojolicious::Plugin::Yancy

Expand Down
31 changes: 20 additions & 11 deletions lib/Mojolicious/Plugin/Yancy.pm
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ By default, the L<Yancy::Plugin::Form::Bootstrap4> form plugin is
loaded. You can override this with your own form plugin. See
L<Yancy::Plugin::Form> for more information.

=head2 yancy.file
=head2 File storage

By default, the L<Yancy::Plugin::File> plugin is loaded to handle file
uploading and file management. The default path for file uploads is
C<$MOJO_HOME/public/uploads>. You can override this with your own file
plugin. See L<Yancy::Plugin::File> for more information.
By default, the editor stores uploaded files in a L<Yancy::Storage>
rooted at C<$MOJO_HOME/public/uploads>. You can override this with
C<< editor => { storage => ... } >>. See L<Yancy::Storage> for more
information.

=head2 yancy.plugin

Expand Down Expand Up @@ -165,6 +165,7 @@ use Mojo::File qw( path );
use Mojo::Loader qw( load_class );
use Yancy::Util qw( load_backend curry copy_inline_refs is_type json_validator );
use Yancy::Model;
use Yancy::Storage;
use Storable qw( dclone );
use Scalar::Util qw( blessed );

Expand Down Expand Up @@ -215,15 +216,23 @@ sub register {
# override this one
# XXX: Form and File should both be part of the content plugin
$app->plugin( 'Yancy::Plugin::Form::Bootstrap4' );
$app->plugin( 'Yancy::Plugin::File' => {
path => $app->home->child( 'public/uploads' ),
} );

# Add the default editor unless the user explicitly disables it
if ( !exists $config->{editor} || defined $config->{editor} ) {
$app->plugin( 'Yancy::Plugin::Editor' => {
%{ $config->{editor} // {} },
} );
my $editor = $config->{editor} // {};
# The editor must be told how to check logins before it is added
if ( !$editor->{users} && !$editor->{allow_anonymous} ) {
$app->log->warn(
'Yancy editor not loaded: Set `editor => { users => $user_model }` to require logins,'
. ' or `editor => { allow_anonymous => 1 }` to allow anyone to edit',
);
}
else {
$app->plugin( 'Yancy::Plugin::Editor' => {
storage => Yancy::Storage->new( $app->home->child( 'public/uploads' ) ),
%$editor,
} );
}
}
}

Expand Down
71 changes: 48 additions & 23 deletions lib/Yancy/Auth.pm
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,25 @@ sub new($class, $app, @args) {
push @{$app->renderer->paths}, "$templates";
}

# Auth templates use the `l` helper for translated strings
if (!$app->renderer->get_helper('l')) {
$app->plugin('I18N', { namespace => 'Yancy::I18N' });
}

# TODO: Auth sets up routes in $app

# Add a logout route for the user model, unless another auth already did
if ( !$app->routes->find('auth.logout') ) {
my $model = $self->model;
$app->routes->any( [qw( GET POST )] => '/auth/logout' )->to( cb => sub ($c) {
$model->end_session( $c );
my $to = $c->req->param( 'return_to' );
$to = '/' unless $self->is_local_path( $to );
$c->res->headers->location( $to );
return $c->rendered( 303 );
} )->name('auth.logout');
}

$self->model->add_auth( $self );
return $self;
}
Expand All @@ -48,43 +65,51 @@ sub form_for( $self, $c, $login ) {
# If $login is provided, do not make the user type it in again.
}

=method is_local_path

my $ok = $auth->is_local_path( $url );

Returns true if the given URL is a path on this site that is safe to redirect
to. It must start with a single C</>, and must not contain backslashes or
control characters, which browsers may treat as a C<//host> URL.
L<http://cwe.mitre.org/data/definitions/601.html>

=cut

sub is_local_path( $self, $url ) {
return defined $url
&& $url =~ m{^/(?![/\\])}
&& $url !~ m{[\\\x00-\x1f\x7f]};
}

sub return_to( $self, $c ) {
my $return_to
my $referrer = $c->req->headers->referrer;
my ( $return_to ) = grep { $self->is_local_path( $_ ) } (
# If we've specified one, go there directly
= $c->req->param( 'return_to' )
? $c->req->param( 'return_to' )
$c->req->param( 'return_to' ),
# Check flash storage, perhaps from a redirect to the login form
: $c->flash('return_to') ? $c->flash('return_to')
$c->flash('return_to'),
# If this is the login page, go back to referer
: $c->current_route =~ /^auth\./
&& $c->req->headers->referrer
&& $c->req->headers->referrer !~ m{^(?:\w+:|//)}
? $c->req->headers->referrer
( ( $c->current_route // '' ) =~ /^auth\./ ? $referrer : () ),
# Otherwise, return the user here
: ( $c->req->url->path || '/' )
;
if ( $return_to =~ m{^(?:\w+:|//)} ) {
die q{`return_to` can not contain URL scheme or host},
}
return $return_to;
$c->req->url->path->to_string,
);
return $return_to // '/';
}

sub start_session( $self, $c, $login ) {
# Start a session for the user
# XXX: Set "current user" in session
# XXX: Set last login datetime
# XXX: Set last used provider
# XXX: Set cookie to make auth easier next time

my $to = $c->req->param( 'return_to' ) || '/';

# Do not allow return_to to redirect the user to another site.
# http://cwe.mitre.org/data/definitions/601.html
if ( $to =~ m{^(?:\w+:|//)} ) {
return $c->reply->exception(
q{`return_to` can not contain URL scheme or host},
);
}
my $to = $c->req->param( 'return_to' );
$to = '/' unless $self->is_local_path( $to );

my $user = $self->get_user( $login )
// die qq{Can not start session for unknown user "$login"\n};
$self->model->start_session( $c, $user->{user_id} );

$c->res->headers->location( $to );
return $c->rendered( 303 );
Expand Down
4 changes: 2 additions & 2 deletions lib/Yancy/Auth/Password.pm
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ sub _build_digest_config_string($config) {
}

sub _check_pass($self, $c, $login, $input_password) {
my $user = $self->get_user( $login );
my $user = $self->get_user( $login ) // return undef;
my $stored_password = $self->attribute( $user->{user_id}, 'password' );

if ( !$stored_password ) {
Expand All @@ -233,7 +233,7 @@ sub _check_pass($self, $c, $login, $input_password) {
if ( $success && ( $user_digest_config_string ne $default_config_string ) ) {
# We need to re-create the user's password field using the new
# settings
$self->_set_password( $login, $input_password );
$self->set_password( $login, $input_password );
}

return $success;
Expand Down
6 changes: 3 additions & 3 deletions lib/Yancy/Auth/templates/auth/password.html.ep
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<form action="<%= $auth->url_for($c) %>" method="POST">
<input type="hidden" name="return_to" value="<%= stash 'return_to' %>" />
<div class="form-group">
<label for="yancy-login">Login</label>
<label for="yancy-login"><%= l 'Login' %></label>
<input class="form-control" id="yancy-login" name="login"
placeholder="Login" value="<%= stash 'login' %>"
>
</div>
<div class="form-group">
<label for="yancy-password">Password</label>
<label for="yancy-password"><%= l 'Password' %></label>
<input type="password" class="form-control" id="yancy-password" name="password" placeholder="Password">
</div>
<button class="btn btn-primary">Login</button>
<button class="btn btn-primary"><%= l 'Login' %></button>
</form>
38 changes: 35 additions & 3 deletions lib/Yancy/Plugin/Editor.pm
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,25 @@ The L<Yancy::Model> to use for this editor.

The L<Yancy::Storage> to use for this editor.

=head2 users

The L<Yancy::User> model to check logins with. Only logged-in users can use
the editor. Requests to the editor API and storage from anyone else get a
C<401> response, and the editor page redirects to the first
L<auth|Yancy::User/auths> registered with the model (or responds with
C<401> if there is none).

Either this or L</allow_anonymous> is required.

=head2 allow_anonymous

Set to true to allow anyone to use the editor without logging in. B<Only use
this for development, or with a L</route> that already requires a login>.

=head2 route

A base route to add the editor to. This allows you to customize the URL
and add authentication or authorization. Defaults to C</yancy> with no
authentication.
and add your own authentication or authorization. Defaults to C</yancy>.

This can be a string or a L<Mojolicious::Routes::Route> object.

Expand Down Expand Up @@ -63,9 +77,28 @@ sub register($self, $app, $config) {

my $model = $config->{model};
my $storage = $config->{storage};
my $users = $config->{users};
if ( !$users && !$config->{allow_anonymous} ) {
die "Yancy::Plugin::Editor requires a `users` model to check logins, or `allow_anonymous => 1`\n";
}

my $route = blessed $config->{route} ? $config->{route} : $app->routes->any( $config->{route} // '/yancy' );
$route->to( return_to => $config->{return_to} // '/' );
my $base = $route->render({});

if ( $users ) {
$route = $route->under( sub ($c) {
return 1 if $users->current_user( $c );
my $path = $c->req->url->path->to_string;
my ( $auth ) = @{ $users->auths };
if ( !$auth || $path =~ m{^\Q$base\E/(?:api|storage)(?:/|$)} ) {
$c->render( status => 401, json => { errors => [ { message => 'Login required' } ] } );
return undef;
}
$c->redirect_to( $auth->url_for( $c )->query( return_to => $c->req->url->path_query ) );
return undef;
} );
}

push @{$app->renderer->paths}, path(__FILE__)->dirname->sibling('Editor', 'templates');
push @{$app->static->paths}, path(__FILE__)->dirname->sibling('Editor', 'dist');
Expand All @@ -89,7 +122,6 @@ sub register($self, $app, $config) {
$route->any( ['DELETE'] => '/storage/*id', { hidden => 1, storage => $storage } )->to( 'storage#delete' );

# Route(s) to view the editor
my $base = $route->render({});
$route->get( '/*rest', { rest => '', hidden => 1, base => $base, template => 'yancy/editor' }, sub($c) {
if (!$c->stash('rest') && !$c->req->url->path->trailing_slash) {
$c->res->code(308);
Expand Down
77 changes: 77 additions & 0 deletions lib/Yancy/User.pm
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,86 @@ sub new($class, @args) {
return $self;
}

=method add_auth

Register a L<Yancy::Auth> with this model. This is called by L<Yancy::Auth>
when it is created.

=cut

sub add_auth( $self, $auth ) {
# XXX: Make sure auth provider_name is unique!
push @{ $self->_auths }, $auth;
}

=method auths

Returns an arrayref of the L<Yancy::Auth> objects registered with this model.

=cut

sub auths( $self ) {
return $self->_auths;
}

=method start_session

my $session_id = $users->start_session( $c, $user_id );

Start a new session for the given user. Creates a row in the C<sessions>
table and stores its ID in the L<Mojolicious session|Mojolicious::Controller/session>.

=cut

sub start_session( $self, $c, $user_id ) {
my $now = _now();
my $session_id = $self->schema('sessions')->create({
user_id => $user_id,
created_datetime => $now,
last_activity_datetime => $now,
});
delete $c->stash->{'yancy.current_user'};
$c->session( yancy_session_id => $session_id );
return $session_id;
}

=method current_user

my $user = $users->current_user( $c );

Returns the user for the current session, or C<undef> if there is no
active session.

=cut

sub current_user( $self, $c ) {
return $c->stash->{'yancy.current_user'} //= do {
my $session_id = $c->session('yancy_session_id');
my $session = defined $session_id ? $self->schema('sessions')->get($session_id) : undef;
$session && !$session->{expired_datetime}
? $self->schema('users')->get($session->{user_id})
: undef;
};
}

=method end_session

$users->end_session( $c );

End the current session, if any.

=cut

sub end_session( $self, $c ) {
delete $c->stash->{'yancy.current_user'};
my $session_id = delete $c->session->{yancy_session_id} // return;
$self->schema('sessions')->set($session_id, { expired_datetime => _now() });
return;
}

sub _now {
my @t = gmtime;
return sprintf '%04d-%02d-%02d %02d:%02d:%02d', $t[5] + 1900, $t[4] + 1, @t[3, 2, 1, 0];
}

1;
Expand Down
2 changes: 2 additions & 0 deletions myapp.pl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
plugin 'Yancy::Plugin::Editor', {
model => $model,
storage => $storage,
# Development app: Allow editing without logging in
allow_anonymous => 1,
};

# TODO: 'Mojolicious::Plugin::Yancy' is combination of above things
Expand Down
Loading
Loading