use strict; package TrsrDB::HTTP; use TrsrDB::Error; use Mojolicious 6.0; use Mojolicious::Sessions; use Mojo::Base 'Mojolicious'; use POSIX qw(strftime); has db => sub { my $db; eval q{use TrsrDB \$db} or $@ && die $@; return $db; }; # This method will run once at server start sub startup { my $self = shift; $self->secrets([rand]); $self->sessions->cookie_name('TrsrDB'); $self->config( hypnotoad => { listen => [ $ENV{MOJO_LISTEN} ], pid_file => $ENV{PIDFILE}, workers => 2, }); $self->defaults( layout => 'default', user => undef, ); if ( my $l = $ENV{LOG} ) { use Mojo::Log; open my $fh, '>', $l or die "Could not open logfile $l to write: $!"; $self->log( Mojo::Log->new( handle => $fh, level => 'warn' ) ); } unshift @{$self->static->paths}, $self->home->rel_dir('site'); $self->helper( 'reply.client_error' => \&prepare_client_error ); $self->hook( before_render => \&restapi_reply_jsonifier ); # Router my $r = $self->routes->under(\&initialize_stash); $r->any( [qw/GET POST/] => "/login" )->to("user#login", retry_msg => 0 ); my $auth = $r->under(\&require_user_otherwise_login_or_fail); $auth->get( '/logout' )->to("user#logout"); my $admin = $auth->under(sub { shift->stash('grade') > 1 }); $admin->any('/admin')->to('admin#dash'); $admin->post('/:account/in')->to('credit#upsert'); $admin->post('/:account/out')->to('debit#upsert'); $admin->get('/:account/credits')->to('credit#list'); $admin->get('/:account/debits')->to('debit#list'); $admin->post('/:account/transfer')->to('account#transfer'); $admin->any( [qw/GET POST PATCH/] => '/credit/:id' )->to('credit#upsert'); $admin->post('/credit')->to('credit#upsert'); $admin->any( [qw/GET POST PATCH/] => '/debit/*id' )->to('debit#upsert'); $admin->post('/debit')->to('debit#upsert'); $admin->get('/:action')->to(controller => 'admin'); $auth->get('/')->to('account#list')->name('home'); my $check = $auth->under(sub { shift->stash('grade') })->get('/'); $check->get('/bankStatement')->to(sub { my $c = shift; $c->stash( records => $c->app->db->resultset("ReconstructedBankStatement") ); $c->render('bankStatement'); }); my $account = $auth->get('/:account')->under(sub { my $c = shift; my $account = $c->stash('account'); if ( my $acc = $c->app->db->resultset('Account')->find($account) ) { $c->stash( account => $acc ); $account = $acc; } else { $c->reply->not_found; return; } return $account->type ? $c->stash('grade') : 1; }); $account->get('/in')->to("credit#upsert"); $account->get('/out')->to("debit#upsert"); $account->get('/:action')->to('account#'); } my $started_time; BEGIN { $started_time = scalar localtime(); } sub get_started_time { $started_time; } sub initialize_stash { my $c = shift; my $ct = $c->req->headers->content_type; $c->stash( is_restapi_req => $c->accepts('', 'json' ) || $ct && $ct ne 'application/x-www-form-urlencoded', current_time => strftime('%Y-%m-%d %H:%M:%S', localtime time), ); return 1; } sub require_user_otherwise_login_or_fail { my $c = shift; my $is_restapi_req = $c->stash('is_restapi_req'); if ( my $u = authenticate_user($c) ) { $c->stash( user => $u ); $c->stash( grade => $u->grade ); return 1; } elsif ( !$is_restapi_req && $c->req->method eq "GET" ) { $c->redirect_to("/login"); } else { $c->res->code(401); } return undef; } # Rely on Mojolicious in that the session cookie be cryptographically # protected against manipulation (HMAC-SHA1 signature). Hence, if the # user id is defined, the user has certainly logged in properly. # Refer to `perldoc Mojolicious::Controller` if interested. # If the REST application programing interface is used, there is no # cookie. To stay "RESTful", we rely on HTTP header "Authentification". sub authenticate_user { my $c = shift; my ($user, my $password) = split /:/, ( $c->req->url->to_abs->userinfo // q{} ), 2; my $further_check; if ( $user ) { $further_check = 1 } elsif ( $user = $c->session('user_id') ) {} $user &&= $c->app->db->user($user) or return; if ( $further_check ) { $user->password_equals($password) or return; $c->stash("user" => $user); } return $user; } sub prepare_client_error { my $c = shift; my $x = @_ > 1 ? { @_ } : shift; my %args; # Case 1: We have got a genuine application error object of # which we know the interface. if ( (my $xclass = ref $x) =~ s{^TrsrDB::Error\b}{} ) { # consider rather Scalar::Util::blessed ... Yes, I did $xclass =~ s{^::}{}; %args = ( %{ $x->dump(0) }, error => $xclass || "General error", ); } # Case 2: We have got a plain hash of arguments to use directly elsif ( ref $x eq 'HASH' ) { %args = ( (map { $_ => undef } qw(message error)), %$x); } # Otherwise, we have got an exception thrown from other, third-party # code. You should design your production-mode exception template so # that it displays only $error and $message, not $exception, because # this might allow potential attackers to examine your server's # vulnerabilities. else { my $u = $c->stash("user"); $c->stash( error => "Internal server error", message => $u && $u->can_admin ? (ref $x ? "$x" : $x) : "Oops, something went wrong. (A more detailed " . "error message logged server-side. Ask the admin.)" , ); return; } $c->res->code( delete $args{http_status} // 500 ); return $c->render( template => 'exception.production', %args ); } sub restapi_reply_jsonifier { my ($c, $args) = @_; return if !$c->stash('is_restapi_req') || $args->{json}; my %stash = ( %{ $c->stash }, %$args ); delete @stash{ # general slots of internal interest ... qw(snapshot user template is_restapi_req layout hoster_info cb action controller ), grep { /^mojo\./ } keys %stash }; $args->{json} = \%stash; } sub render_online_help { require Text::Markdown; my $c = shift; my $file = $c->stash("file"); if ( $file =~ m{(^|\/)\.} ) { return $c->reply->not_found; } elsif ( $file =~ m{\.(\w{3,4})$} ) { return $c->reply->static( "../doc/online-help/" . $file ); } $file = $c->app->home->rel_file( "doc/online-help/" . ( $file || "faq" ) . ".md" ); open my $fh, '<', $file or return $c->reply->not_found; binmode $fh, ':utf8'; $c->stash( layout => undef ) if $c->param('bare'); $c->render( template => 'online_help', file => $file ); } 1; __END__