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 }