Verified Commit 8b68d7e8 authored by Luc Didry's avatar Luc Didry
Browse files

Fix #150️ Implement invitations to other people when using LDAP auth

parent e8ff0e46
......@@ -4,6 +4,7 @@ Revision history for Lufi
- Allow to zip the files before upload
- Allow to see what's in zip file on download page
- Allow to individually download files from zip file (only if zip created by Lufi)
- Allow to invite people to send you files on Lufi when using LDAP auth (See #150)
0.03.7 2019-08-01
- Fix missing default values for some settings (mildis)
......
......@@ -110,6 +110,47 @@ sub startup {
$r->post('/logout')
->to('Auth#log_out')
->name('logout');
if (defined $self->config('ldap') && defined $self->config('invitations')) {
# Invitation creation page
$r->get('/invite')
->name('invite')
->to('Invitation#new_invite');
# Send invitation
$r->post('/invite')
->to('Invitation#send_invite');
# Get my invitations
$r->get('/invite/list')
->name('invite_list')
->to('Invitation#my_invitations');
# Delete invitations
$r->post('/invite/list/delete')
->name('invite_list_delete')
->to('Invitation#delete_invitations');
# Resend invitation mail
$r->post('/invite/list/resend')
->name('invite_list_resend')
->to('Invitation#resend_invitations');
# Toggle invitations visibility
$r->post('/invite/list/visibility')
->name('invite_list_visibility')
->to('Invitation#toggle_invitations_visibility');
# I’m a guest
$r->get('/guest/:token')
->name('guest')
->to('Invitation#guest');
# I’m a guest and I sent all my files
$r->post('/guest/:token/send_mail')
->name('guest_send_mail')
->to('Invitation#send_mail_to_ldap_user');
}
}
# About page
......
......@@ -2,6 +2,7 @@ package Lufi::Command::sqliteToOtherDB;
use Mojo::Base 'Mojolicious::Command';
use Lufi::DB::File;
use Lufi::DB::Slice;
use Lufi::DB::Invitation;
use Mojo::SQLite;
use FindBin qw($Bin);
use Term::ProgressBar;
......@@ -31,11 +32,12 @@ sub run {
exit 1;
}
my $sqlite = Mojo::SQLite->new('sqlite:'.$config->{db_path});
my $files = $sqlite->db->select('files', undef)->hashes;
my $slices = $sqlite->db->select('slices', undef)->hashes;
my $sqlite = Mojo::SQLite->new('sqlite:'.$config->{db_path});
my $files = $sqlite->db->select('files', undef)->hashes;
my $slices = $sqlite->db->select('slices', undef)->hashes;
my $invitations = $sqlite->db->select('invitations', undef)->hashes;
my $progress = Term::ProgressBar->new({count => $files->size + $slices->size});
my $progress = Term::ProgressBar->new({count => $files->size + $slices->size + $invitations->size});
$files->each(sub {
my ($file, $num) = @_;
......@@ -72,6 +74,24 @@ sub run {
$progress->update();
});
$invitations->each(sub {
my ($invitation, $num) = @_;
Lufi::DB::Invitation->new(app => $c->app)
->token($invitation->{token})
->ldap_user($invitation->{ldap_user})
->ldap_user_mail($invitation->{ldap_user_mail})
->guest_mail($invitation->{guest_mail})
->created_at($invitation->{created_at})
->expire_at($invitation->{expire_at})
->files_sent_at($invitation->{files_sent_at})
->expend_expire_at($invitation->{expend_expire_at})
->files($invitation->{files})
->show_in_list($invitation->{show_in_list})
->deleted($invitation->{deleted})
->write();
$progress->update();
});
}
=encoding utf8
......
......@@ -4,26 +4,36 @@ use Mojo::Base 'Mojolicious::Controller';
sub login_page {
my $c = shift;
my $redirect = $c->param('redirect') // 'index';
if ($c->is_user_authenticated) {
$c->redirect_to('index');
} else {
$c->render(template => 'login');
$c->render(
template => 'login',
redirect => $redirect
);
}
}
sub login {
my $c = shift;
my $login = $c->param('login');
my $pwd = $c->param('password');
my $login = $c->param('login');
my $pwd = $c->param('password');
my $redirect = $c->param('redirect') // 'index';
if ($c->validation->csrf_protect->has_error('csrf_token')) {
$c->stash(msg => $c->l('Bad CSRF token.'));
$c->render(template => 'login');
} else {
if($c->authenticate($login, $pwd)) {
$c->redirect_to('index');
if ($redirect eq 'invite') {
return $c->redirect_to('invite');
} elsif ($redirect eq 'my_invitations') {
return $c->redirect_to('invite_list');
}
return $c->redirect_to('index');
} else {
$c->stash(msg => $c->l('Please, check your credentials or your right to access this service: unable to authenticate.'));
$c->render(template => 'login');
......
......@@ -24,7 +24,10 @@ sub files {
sub upload {
my $c = shift;
if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated) {
my $invitation;
my $token = $c->session->{guest_token};
$invitation = Lufi::DB::Invitation->new(app => $c->app)->from_token($token) if $token;
if ((!defined($c->config('ldap')) && !defined($c->config('htpasswd'))) || $c->is_user_authenticated || $invitation) {
$c->inactivity_timeout(30000000);
$c->app->log->debug('Client connected');
......@@ -33,6 +36,8 @@ sub upload {
message => sub {
my ($ws, $text) = @_;
my $invit = Lufi::DB::Invitation->new(app => $c->app)->from_token($token) if $token;
my $begin = time;
my ($json) = split('XXMOJOXX', $text, 2);
......@@ -81,7 +86,7 @@ sub upload {
)));
}
# Check against max_size
elsif (defined $c->config('max_file_size')) {
if (defined $c->config('max_file_size')) {
if ($json->{size} > $c->config('max_file_size')) {
$stop = 1;
return $ws->send(decode('UTF-8', encode_json(
......@@ -95,7 +100,7 @@ sub upload {
}
}
# Check that we have enough space (multiplying by 2 since it's encrypted, it takes more place that the original file)
elsif ($json->{part} == 0 && ($json->{size} * 2) >= dfportable($c->config('upload_dir'))->{bavail}) {
if ($json->{part} == 0 && ($json->{size} * 2) >= dfportable($c->config('upload_dir'))->{bavail}) {
$stop = 1;
return $ws->send(decode('UTF-8', encode_json(
{
......@@ -106,6 +111,18 @@ sub upload {
}
)));
}
# Check that the invitation is still valid, but only if it's the first chunk
# (i.e. a new file, we don't want to stop a current uploading)
if ($json->{part} == 0 && $invit && !$invit->is_valid()) {
$stop = 1;
$c->app->log->info(sprintf('Someone (%s) tried to use an expired or deleted invitation.', $invit->guest_mail));
$ws->send(decode('UTF-8', encode_json(
{
success => false,
msg => $c->l('Sorry, your invitation has expired or has been deleted. Please contact %1 to have another invitation.', $invit->ldap_user_mail),
}
)));
}
unless ($stop) {
my $f;
......@@ -142,9 +159,15 @@ sub upload {
}
my $creator = $c->ip;
if (defined($c->config('ldap')) || defined($c->config('htpasswd'))) {
$creator = 'User: '.$c->current_user->{username}.', IP: '.$creator;
# Authenticated user logging
if ((defined($c->config('ldap')) || defined($c->config('htpasswd'))) && !$invitation) {
$creator = sprintf('User: %s, IP: %s', $c->current_user->{username}, $creator);
}
# Guest user logging
if ($invitation) {
$creator = sprintf('User: %s, IP: %s', $invitation->guest_mail, $creator);
}
my $delete_at_first_view = ($json->{del_at_first_view}) ? 1 : 0;
$delete_at_first_view = 1 if $c->app->config('force_burn_after_reading');
$f = Lufi::DB::File->new(app => $c->app)->get_empty()
......@@ -190,23 +213,22 @@ sub upload {
}
}
$ws->send(to_json(
{
success => true,
i => $json->{i},
j => $json->{part},
parts => $json->{total},
short => $f->short,
name => $f->filename,
size => $f->filesize,
del_at_first_view => (($f->delete_at_first_view) ? true : false),
created_at => $f->created_at,
delay => $f->delete_at_day,
token => $f->mod_token,
sent_delay => $json->{delay},
duration => time - $begin
}
));
my $result = {
success => true,
i => $json->{i},
j => $json->{part},
parts => $json->{total},
short => $f->short,
name => $f->filename,
size => $f->filesize,
del_at_first_view => (($f->delete_at_first_view) ? true : false),
created_at => $f->created_at,
delay => $f->delete_at_day,
token => $f->mod_token,
sent_delay => $json->{delay},
duration => time - $begin
};
$ws->send(to_json($result));
} else {
$ws->send(decode('UTF-8', encode_json(
{
......
# vim:set sw=4 ts=4 sts=4 ft=perl expandtab:
package Lufi::Controller::Invitation;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Collection 'c';
use Mojo::File;
use Mojo::JSON qw(true false decode_json encode_json);
use Mojo::URL;
use Email::Valid;
use Lufi::DB::File;
use Lufi::DB::Invitation;
use Date::Format;
sub new_invite {
my $c = shift;
# The `if (defined($c->config('ldap')))` is at the router level in lib/Lufi.pm
if ($c->is_user_authenticated) {
my $mail_attr = $c->config('invitations')->{'mail_attr'} // 'mail';
my $max_expire_at = $c->config('invitations')->{'max_invitation_expiration_delay'} // 30;
my $send_with_user_email = defined $c->config('invitations')->{'send_invitation_with_ldap_user_mail'};
$c->render(
template => 'invitations/invite',
max_expire_at => $max_expire_at,
send_with_user_email => $send_with_user_email,
user_mail => ($send_with_user_email) ? $c->current_user->{$mail_attr} : '',
fails => [],
success => []
);
} else {
$c->redirect_to($c->url_for('login')->query(redirect => 'invite'));
}
}
sub send_invite {
my $c = shift;
my $guest_mail = $c->param('guest_mail');
my $expire_at = $c->param('expire_at');
my $mail_attr = $c->config('invitations')->{'mail_attr'} // 'mail';
my $max_expire_at = $c->config('invitations')->{'max_invitation_expiration_delay'} // 30;
my $send_with_user_email = defined $c->config('invitations')->{'send_invitation_with_ldap_user_mail'};
# The `if (defined($c->config('ldap')))` is at the router level in lib/Lufi.pm
if ($c->is_user_authenticated) {
my @fails = ();
my @success = ();
unless (Email::Valid->address($guest_mail)) {
push @fails, $c->l('The guest email address (%1) is unvalid.', $guest_mail);
}
unless ($expire_at >= 1 && $expire_at <= $max_expire_at) {
push @fails, $c->l('The expiration delay (%1) is not between 1 and %2 days.', $expire_at, $max_expire_at);
}
unless (scalar(@fails)) {
my $invitation = Lufi::DB::Invitation->new(app => $c->app);
my $mail_attr = $c->config('invitations')->{'mail_attr'} // 'mail';
my $expend_expire_at = $c->config('invitations')->{'expend_expire_at'} // 10;
my $token;
do {
$token = $c->create_invitation_token;
} while ($invitation->is_token_used($token));
$invitation = $invitation->from_token($token);
$invitation->ldap_user($c->current_user->{username});
$invitation->ldap_user_mail($c->current_user->{$mail_attr});
$invitation->created_at(time);
$invitation->guest_mail($guest_mail);
$invitation->expire_at($invitation->created_at + 86400 * $expire_at);
$invitation->expend_expire_at($expend_expire_at);
$invitation->show_in_list(1);
$invitation = $invitation->write;
my $from = ($c->config('invitations')->{'send_invitation_with_ldap_user_mail'}) ? $invitation->ldap_user_mail : $c->config('mail_sender');
my $url = $c->url_for('guest', token => $invitation->token)->to_abs;
$c->mail(
from => $from,
to => $invitation->guest_mail,
template => 'invitations/invite',
format => 'mail',
ldap_user => ucfirst($invitation->ldap_user),
url => $url,
invitation => $invitation,
expires => time2str($c->l('%A %d %B %Y at %T'), $invitation->expire_at)
);
push @success, $c->l('Invitation sent to %1.<br> URL: %2', $invitation->guest_mail, $url);
}
$c->render(
template => 'invitations/invite',
max_expire_at => $max_expire_at,
send_with_user_email => $send_with_user_email,
user_mail => ($send_with_user_email) ? $c->current_user->{$mail_attr} : '',
fails => \@fails,
success => \@success
);
} else {
$c->redirect_to('login');
}
}
sub my_invitations {
my $c = shift;
# The `if (defined($c->config('ldap')))` is at the router level in lib/Lufi.pm
if ($c->is_user_authenticated) {
my $invitations = Lufi::DB::Invitation->new(app => $c->app)
->from_user($c->current_user->{username});
$invitations = c() unless $invitations;
$c->render(
template => 'invitations/my_invitations',
invitations => $invitations
);
} else {
$c->redirect_to($c->url_for('login')->query(redirect => 'my_invitations'));
}
}
sub delete_invitations {
my $c = shift;
my @tokens = @{$c->every_param('tokens[]')};
my @result = ();
for my $token (@tokens) {
my $i = Lufi::DB::Invitation->new(app => $c->app)
->from_token($token)
->deleted(1)
->write;
push @result, { msg => $c->l('The invitation %1 has been deleted.', $i->token), token => $i->token, deleted => $i->deleted };
}
$c->render(json => {
success => true,
tokens => \@result
});
}
sub resend_invitations {
my $c = shift;
my @tokens = @{$c->every_param('tokens[]')};
my @success;
my @failures;
for my $token (@tokens) {
my $i = Lufi::DB::Invitation->new(app => $c->app)
->from_token($token);
if ($i->files_sent_at) {
push @failures, $c->l('The invitation %1 can’t be resend: %2 has already sent files.<br>Please create a new invitation.', $i->token, $i->guest_mail);
} else {
if ($c->config('invitations')->{'extend_invitation_expiration_on_resend'}) {
$i->expire_at(time + $i->expire_at - $i->created_at)
->write;
}
my $from = ($c->config('invitations')->{'send_invitation_with_ldap_user_mail'}) ? $i->ldap_user_mail : $c->config('mail_sender');
my $url = $c->url_for('guest', token => $i->token)->to_abs;
my $expire = time2str($c->l('%A %d %B %Y at %T'), $i->expire_at);
$c->mail(
from => $from,
to => $i->guest_mail,
template => 'invitations/invite',
format => 'mail',
ldap_user => ucfirst($i->ldap_user),
url => $url,
invitation => $i,
expires => $expire
);
push @success, { msg => $c->l('Invitation resent to %1.<br> URL: %2', $i->guest_mail, $url), expires => $expire, token => $i->token };
}
}
$c->render(json => {
success => \@success,
failures => \@failures
});
}
sub toggle_invitations_visibility {
my $c = shift;
my @tokens = @{$c->every_param('tokens[]')};
my @result = ();
for my $token (@tokens) {
my $i = Lufi::DB::Invitation->new(app => $c->app)
->from_token($token)
->toggle_visibility;
push @result, { token => $i->token, show => ($i->show_in_list) ? true : false }
}
$c->render(json => {
success => true,
tokens => \@result
});
}
sub guest {
my $c = shift;
my $token = $c->param('token');
my $invitation = Lufi::DB::Invitation->new(app => $c->app)->from_token($token);
if ($invitation) {
if ($invitation->is_valid) {
$c->session->{guest_token} = $token;
$c->session(expires => $invitation->expire_at);
return $c->render(
template => 'index',
invitation => $invitation
);
} else {
$c->stash('expired_or_deleted_invitation' => 1);
}
} else {
$c->stash('invitation_not_found' => 1);
}
return $c->render(template => 'invitations/exception');
}
sub send_mail_to_ldap_user {
my $c = shift;
my $token = $c->param('token');
my $urls = c(@{$c->every_param('urls[]')});
my $invitation = Lufi::DB::Invitation->new(app => $c->app)->from_token($token);
if ($invitation) {
my @files = ();
if ($c->config('invitations')->{'save_files_url_in_db'} && $urls->size) {
my $guest_files = $invitation->files;
if ($guest_files) {
$guest_files = decode_json($guest_files);
} else {
$guest_files = [];
}
push @files, @{$guest_files};
$urls->each(sub {
my ($e, $num) = @_;
$e = decode_json($e);
push @{$guest_files}, $e;
push @files, $e;
});
$invitation->files(encode_json($guest_files));
$invitation->write;
} else {
$urls->each(sub {
push @files, decode_json(shift);
});
}
my $already_notified = 1;
unless ($invitation->files_sent_at) {
$invitation->files_sent_at(time);
$invitation->write;
$already_notified = 0;
}
$c->session(expires => $invitation->files_sent_at + 60 * $invitation->expend_expire_at);
$c->mail(
from => $c->config('mail_sender'),
to => $invitation->ldap_user_mail,
template => 'invitations/notification_files_sent',
format => 'mail',
files => c(@files),
invitation => $invitation,
already_notified => $already_notified
);
return $c->render(
json => {
success => true,
msg => $c->l('The URLs of your files have been sent by email to %1.', $invitation->ldap_user_mail)
}
);
} else {
return $c->render(
json => {
success => false,
msg => $c->l('Sorry, the invitation doesn’t exist. Are you sure you are on the right URL?')
}
);
}
}
1;
......@@ -8,9 +8,7 @@ has 'token';
has 'ldap_user';
has 'ldap_user_mail';
has 'guest_mail';
has 'created_at' => sub {
return time;
};
has 'created_at';
has 'expire_at';
has 'files_sent_at';
has 'expend_expire_at';
......@@ -199,6 +197,32 @@ sub show {
return $c;
}
=head2 toggle_visibility
=over 1
=item B<Usage> : C<$c-E<gt>toggle_visibility>
=item B<Arguments> : none
=item B<Purpose> : toggle the C<show_in_list> flag
=item B<Returns> : the db accessor object
=back
=cut
sub toggle_visibility {
my $c = shift;
if ($c->show_in_list) {
return $c->hide;
} else {
return $c->show;
}
}
=head2 write
=over 1
......@@ -236,7 +260,7 @@ sub write {
=item B<Arguments> : string
=item B<Purpose> : find an invitation in the database from its token attribute
=item B<Purpose> : find an invitation in the database from its C<token> attribute