RSS Git Download  Clone
Raw Blame History
package Routes::Moongate;
use Dancer2 appname => 'DocsLib';

use v5.34.0;
use Path::Tiny;
use Data::Printer;

use App::Model; # D2 plugin, provides 'model' keyword
use Dancer2::Plugin::Deferred;
use Dancer2::Plugin::Auth::Tiny;

my $PREFIX = 'moongate'; 
prefix "/$PREFIX";

my $docs_path = path( config->{documents_path}, $PREFIX ); # debug $docs_path;
# set path to documents folder:
model->moongate->set_docs_path($docs_path);

hook before_template_render => sub {
    my $tokens = shift; # need to have unique token name across all route files 
    my %h = (
        home     => '/',
        new      => '/new_document',
        edit     => '/edit',
        create   => '/create',
        search   => '/search',
        update   => '/update',
        summary  => '/summary',
        document => '/id',
        download => '/download',
    );
    $tokens->{uri_for_section}->{$PREFIX}->{$_}
        = uri_for('/'. $PREFIX . $h{$_}) for keys %h; # p $tokens;
};

=begin # rules for route names & uri_for_route:
cannot use method 'any' with route names: "Route with this name ($name) already exists"
   probably when route name is registered for GET, then for POST/HEAD/etc
cannot use uri_for_route in any template loaded after forward (data is deleted)
=cut

# these routes need unique (to DocsLib app) route-names (for redirect):
get  moongate_home    => '/' => needs login  => \&home;
get  moongate_entry   => '/id/:id[Int]'      => \&get_document;
# these routes need permissions:
get  '/edit/:id[Int]'     => needs login => \&edit_document;
post '/create'            => needs login => \&create_document;
post '/update/:id[Int]'   => needs login => \&update_document;
# these routes don't need names or permissions
get  '/new_document'      => \&new_document;
get  '/summary'           => \&summary;
get  '/download/:id[Str]' => \&download;
post '/search'            => \&search;

# ==============================================================================

sub flash { deferred @_ }

sub home { template home => { title => 'Moongate' } };

sub new_document {
    var next_route => 'create';
    template $PREFIX.'/record.tt', { prefix => $PREFIX };
};

sub get_document {
    my $id = route_parameters->get('id');
    my $rec = model->moongate->get_document($id); # p $rec; # AoH
    vars->{next_route} ||= 'edit/'.$id; # may already be set by edit_document()
    template $PREFIX.'/record.tt', { entry => $rec };
};

sub edit_document {
    my $id = route_parameters->get('id');
	var next_route => 'update/'.$id;
    my $route = '/'.$PREFIX.'/id/'.$id;
	forward $route, {}, { method => 'GET' };
};

sub create_document {
    my $params = request->parameters->as_hashref; # p $params;
    # get upload data - new entry only, passed as param if edit:
    my $data_file = upload('filename'); # p $data_file;

    my $res = model->moongate->save_document($params, $data_file); # p $res;
	if ( $res->{error} ) {
		var input_error => $res->{error};
        template $PREFIX.'/record.tt', { entry => $params };
    }
	else {
        my $action = $params->{id} ? 'update_success' : 'input_success'; # update if id supplied
		flash ( $action => 1 ); # doesn't need message
        if ($data_file) { # upload file to docs dir if uploaded:
			my $filepath = path($docs_path, $params->{filename});
			$data_file->copy_to( $filepath );
		}
		redirect uri_for_route( 'moongate_entry', { id => $res->{id} } );
	}
};

sub update_document { # just captures document_id & forwards:
	forward '/'.$PREFIX.'/create', { id => route_parameters->get('id') };
};

sub summary {
    my $rec = model->moongate->get_all_documents; # p $rec; # AoH
    template $PREFIX.'/summary.tt', { records => $rec };
};

sub search {
	my $str = body_parameters->get('search'); # =~ s{\s+}{}gr; sometimes need space
	my $rec = model->moongate->find_documents($str); # p $rec; # AoH
    template $PREFIX.'/summary.tt', { records => $rec };
};

sub download {
    my $dir = Path::Tiny->new($docs_path);
    # normalize to absolute path unless already (returns 'not found' using relative paths):
    $dir = $dir->absolute( config->{appdir} ) unless $dir->is_absolute;

    my $doc  = route_parameters->get('id'); debug $doc;
    my $file = $dir->child($doc); debug "Resolved file path: $file";

    my $validation = _validate_download($dir, $doc, $file); # returns 'OK', or arrayref
    if ( ref $validation eq 'ARRAY' ) {
        my ($error, $code) = @$validation;
        warning "failed validation of download: $doc ($error)";
        send_error $error, $code;
    }
    send_file $file->stringify, system_path => 1;
};

sub _validate_download {
    my ($dir, $filename, $filepath) = @_;
    # Validation: reject empty, dot, or path traversal attempts
    return [ 'Invalid file name', 400 ] if (
        !defined $filename    ||
        $filename eq ''       ||
        $filename =~ m{[\\/]} ||
        $filename =~ m{\.\.|^\.|/$}
    );
    # Ensure file is within the allowed directory
    return [ 'Access denied', 403 ] unless
        $filepath->realpath->subsumes($dir->realpath);
    # Ensure filel exists
    return [ 'File not found', 404 ] unless -e $filepath;
    return 'OK'; # no error
}

true;