RSS Git Download  Clone
Raw Blame History
package Routes;

use 5.24.0; # say
use Data::Printer;
use File::Slurper 'read_text';
use Time::HiRes qw(gettimeofday tv_interval);

use Dancer2;
use Dancer2::Plugin::Deferred;
use Dancer2::Plugin::Database;
use Dancer2::Plugin::Auth::Tiny;
use Dancer2::Plugin::CryptPassphrase; # this is (deliberately) slow due to hashing

use App::Model;

use constant PERLVERSION => $^V;
use if PERLVERSION lt v5.36.0, experimental => 'signatures';

our $VERSION = '0.1';

# add 'on_connect_do' statement from db/init_db.sql (text too long for
# config.yml without using line-break commands)
my $cfg = config(); # OK to make this global
$cfg->{plugins}->{Database}->{on_connect_do} = read_text( setting('db_init') ); # p $cfg;

# turn on/off DBI query output to console if supplied as command-line option:
database->trace('SQL|'.$ENV{DBI_TRACE}) if defined $ENV{DBI_TRACE}; # could have value '0'

set auto_page => 1; # maybe not in use, but useful anyway

my $app = App::Model->new( dbh => database(), dsl => dsl() ); # p $app->model;

hook before => sub {
	var t0 => [gettimeofday];
};

hook before_template_render => sub {
    my $tokens = shift;
    $tokens->{css_url} = request->base . 'css'; # uri_for's called directly in .tt
};

get '/' => needs login => sub { # debug 'here';
    template home => { title => 'DocsLibrary' };
};

# for redirect after successful submit:
get '/id/:id[Int]' => sub {
    my $id = route_parameters->get('id');
    my $entry = $app->get_document($id);
    template home => { title => 'DocsLibrary', entry => $entry };
};

get '/login' => sub { # my $p = params(); p $p;
    my $return_url = query_parameters->get('return_url'); # set by Auth::Tiny
    template login => { title => 'Login page', return_url => $return_url }
};

post '/search' => sub {
	my $str = body_parameters->get('search') =~ s{\s+}{}gr; # say $keywords;
	my $res = $app->find_documents($str); p $res;
	template home => { title => 'DocsLibrary', entries => $res };
};

post '/new_document' => sub {
	# get keywords, remove any spaces:
	my $keywords = body_parameters->get('keywords') =~ s{\s+}{}gr; # say $keywords;
    my $content  = body_parameters->get('content');
	my $title    = body_parameters->get('title');

	my %d = (
		keywords => $keywords,
		content  => $content,
		title    => $title,
	);
    my $res = $app->save_document(\%d); # p $res;
	if ( $res->{error} ) {
		var input_error => $res->{error};
		forward '/', { entry => \%d }, { method => 'GET' };
    }
	else {
		deferred input_success => 1; # doesn't need message
		redirect uri_for '/id/' . $res->{id};
	}
};

# this isn't needed except for testing:
get '/logout' => sub { app->destroy_session; redirect '/' };

post '/login' => sub { # my $p = params(); p $p;
    my $password   = body_parameters->get('password');
    my $return_url = body_parameters->get('return_url');

	if ( my $username = _authenticate_user($password) ) {
        app->change_session_id;
        session user => $username; # Plugin::Auth::Tiny requires session id = 'user'
        info "$username successfully logged in";
        redirect $return_url || '/';
    }
    else {
        warning "failed login attempt for $username";
        template login => { login_error => 1, return_url => $return_url };
    }
};

true;

#  using PBKDF2 for development, faster but less secure than Argon2 for deployment
sub _authenticate_user ($password) {
	my $username = body_parameters->get('username');

	if ( app->environment eq 'development' ) { # use faster PBKDF2 method
		my $user = setting 'user'; # in development_local.yml
		$username ||= $user->{name}; # don't need to provide it for dev
		return $username if $app->verify_password( $password, $user->{pwd} );
	}
	else { # Plugin::CryptPassphrase (uses more secure argon2 auth)
		my $user = $app->find_user($username);
		return $username if verify_password( $password, $user->{password} );
	}
	return undef; # failed authentication
}