File Coverage

blib/lib/Mojolicious/Plugin/RoutesAuthDBI.pm
Criterion Covered Total %
statement 18 96 18.7
branch 0 52 0.0
condition 0 104 0.0
subroutine 6 12 50.0
pod 1 4 25.0
total 25 268 9.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::RoutesAuthDBI;
2 1     1   69161 use Mojo::Base 'Mojolicious::Plugin::Authentication';
  1         201910  
  1         9  
3 1     1   3592 use Mojolicious::Plugin::RoutesAuthDBI::Util qw(load_class);
  1         5  
  1         71  
4 1     1   9 use Mojo::Util qw(hmac_sha1_sum);
  1         3  
  1         46  
5 1     1   654 use Hash::Merge qw( merge );
  1         10984  
  1         69  
6 1     1   16 use Scalar::Util 'weaken';
  1         2  
  1         49  
7              
8 1     1   9 use constant PKG => __PACKAGE__;
  1         2  
  1         3644  
9              
10             has [qw(app dbh conf)];
11              
12             has default => sub {
13             my $self = shift;
14             require Mojolicious::Plugin::RoutesAuthDBI::Schema;
15             {
16             auth => {
17             stash_key => PKG."__user__",
18             current_user_fn => 'auth_user',# helper
19             load_user => \&load_user,
20             validate_user => \&validate_user,
21             },
22            
23             access => {
24             namespace => PKG,
25             module => 'Access',
26             fail_auth_cb => sub {
27             shift->render(status => 401, format=>'txt', text=>"Please sign in.\n");
28             },
29             fail_access_cb => sub {
30             shift->render(status => 403, format=>'txt', text=>"You don`t have access on this route (url, action).\n");
31             },
32             import => [qw(load_user validate_user)],
33             },
34            
35             admin => {
36             namespace => PKG,
37             controller => 'Admin',
38             prefix => lc($self->conf->{admin}{controller} || 'admin'),
39             trust => hmac_sha1_sum('admin', $self->app->secrets->[0]),
40             role_admin => 'administrators',
41             },
42            
43             oauth => {
44             namespace => PKG,
45             controller => 'OAuth',
46             fail_auth_cb => sub {shift->render(format=>'txt', text=>"@_")},
47             },
48            
49             template => $Mojolicious::Plugin::RoutesAuthDBI::Schema::defaults,
50            
51             model_namespace => PKG.'::Model',
52            
53             guest => {# from Mojolicious::Plugin::Authentication
54             #~ autoload_user => 1,
55             session_key => 'guest_data',
56             stash_key => PKG."__guest__",
57             #~ current_user_fn => 'current_guest',# helper
58             #~ load_user => \&load_guest,
59             #~ validate_user => not need
60             #~ fail_render => not need
61             #######end Mojolicious::Plugin::Authentication conf#######
62             namespace => PKG,
63             module => 'Guest',
64             #~ import => [qw(load_guest)],
65             },
66             log=>{
67             namespace => PKG,
68             module => 'Log',
69             disabled=>0,
70             },
71             };
72             };# end defaults
73              
74             has merge_conf => sub {#hashref
75             my $self = shift;
76             merge($self->conf, $self->default);
77             };
78              
79             has access => sub {# object
80             my $self = shift;
81             weaken $self;
82             my $conf = $self->merge_conf->{'access'};
83             @{$self->merge_conf->{template}{tables}}{keys %{$conf->{tables}}} = values %{$conf->{tables}}
84             if $conf->{tables};
85             my $class = load_class($conf);
86             $class->import( @{ $conf->{import} });
87             $class->new(app=>$self->app, plugin=>$self,);
88             }, weak => 1;
89              
90             has admin => sub {# object
91             my $self = shift;
92             my $conf = $self->merge_conf->{'admin'};
93             @{$self->merge_conf->{template}{tables}}{keys %{$conf->{tables}}} = values %{$conf->{tables}}
94             if $conf->{tables};
95             load_class($conf)->init(%$conf, app=>$self->app, plugin=>$self,);
96             }, weak => 1;
97              
98             has oauth => sub {
99             my $self = shift;
100             my $conf = $self->merge_conf->{'oauth'};
101             @{$self->merge_conf->{template}{tables}}{keys %{$conf->{tables}}} = values %{$conf->{tables}}
102             if $conf->{tables};
103             load_class($conf)->init(%$conf, app=>$self->app, plugin=>$self, model=>$self->model($conf->{controller}),);
104             }, weak => 1;
105              
106             has guest => sub {# object
107             my $self = shift;
108             my $conf = $self->merge_conf->{'guest'};
109             @{$self->merge_conf->{template}{tables}}{keys %{$conf->{tables}}} = values %{$conf->{tables}}
110             if $conf->{tables};
111            
112             $self->merge_conf->{template}{tables}{guests} = $conf->{table}
113             if $conf->{table};
114            
115             my $class = load_class($conf);
116             $class->new( %$conf, app=>$self->app, plugin=>$self, model=>$self->model($conf->{module}), );
117             }, weak => 1;
118              
119             has log => sub {# object
120             my $self = shift;
121             my $conf = $self->merge_conf->{'log'};
122            
123             @{$self->merge_conf->{template}{tables}}{keys %{$conf->{tables}}} = values %{$conf->{tables}}
124             if $conf->{tables};
125            
126             $self->merge_conf->{template}{tables}{logs} = $conf->{table}
127             if $conf->{table};
128            
129             my $class = load_class($conf);
130             $class->new( %$conf, app=>$self->app, plugin=>$self, model=>$self->model($conf->{module}), )
131             unless $conf->{disabled};
132             }, weak => 1;
133              
134              
135             sub register {
136 0     0 1   my $self = shift;
137 0           $self->app(shift);
138 0           $self->conf(shift); # global
139            
140 0   0       $self->dbh($self->conf->{dbh} || $self->app->dbh);
141 0 0         $self->dbh($self->dbh->($self->app))
142             if ref($self->dbh) eq 'CODE';
143 0 0         die "Plugin must work with dbh, see SYNOPSIS" unless $self->dbh;
144            
145             # init base model
146 0           load_class($self->merge_conf->{model_namespace}."::Base")->singleton(dbh=>$self->dbh, template_vars=>$self->merge_conf->{template}, mt=>{tag_start=>'{%', tag_end=>'%}'});
147            
148 0           my $access = $self->access;
149            
150             die "Plugin [Authentication] already loaded"
151 0 0         if $self->app->renderer->helpers->{'authenticate'};
152            
153 0           $self->SUPER::register($self->app, $self->merge_conf->{auth});
154 0           $self->app->plugin('HeaderCondition');# routes host_re
155            
156 0           weaken $self;
157 0     0     $self->app->routes->add_condition(access => sub {$self->cond_access(@_)});
  0            
158 0           $access->apply_ns();
159 0           $access->apply_route($_) for @{ $access->routes };
  0            
160            
161 0 0         if ($self->conf->{oauth}) {
162 0           my $oauth = $self->oauth;
163 0           $access->apply_route($_) for $oauth->_routes;
164             }
165            
166 0 0 0       if ($self->conf->{admin} && ref $self->conf->{admin} eq 'HASH' && keys %{$self->conf->{admin}}) {
  0   0        
167 0           my $admin = $self->admin;
168 0           $access->apply_route($_) for $admin->self_routes;
169             }
170            
171             $self->guest
172 0 0         if $self->conf->{guest};
173            
174             $self->log
175 0 0         if $self->conf->{log};
176            
177 0           weaken $access;
178 0     0     $self->app->helper('access', sub {$access});
  0            
179            
180 0           return $self, $access;
181              
182             }
183              
184             sub cond_access {# add_condition
185 0     0 0   my $self= shift;
186 0           my ($route, $c, $captures, $args) = @_;# $args - это маршрут-хэш из запроса БД или хэш-конфиг из кода
187 0           $route->{(PKG)}{route} = $args;# может пригодиться: $c->match->endpoint->{'Mojolicious::Plugin::RoutesAuthDBI'}...
188 0           my $conf = $self->merge_conf;
189 0           my $app = $c->app;
190 0           my $access = $self->access;
191             #~ $app->log->debug($c->dumper($route));#$route->pattern->defaults
192            
193 0           my $auth_helper = $conf->{auth}{current_user_fn};
194 0           my $u = $c->$auth_helper;
195 0           my $fail_auth_cb = $conf->{access}{fail_auth_cb};
196            
197 0 0         if (ref $args eq 'CODE') {
198 0 0 0       $args->($u, @_)
      0        
199             or $self->deny_log($route, $args, $u, $c)
200             and $c->$fail_auth_cb()
201             and return undef;
202 0           $app->log->debug(sprintf(qq[Access allow [%s] by callback condition],
203             $route->pattern->unparsed,
204             ));
205 0           return 0x01;
206             }
207            
208             $app->log->debug(
209             sprintf(qq[Access allow [%s] for none {auth} and none {role} and none {guest}], $route->pattern->unparsed)
210             )
211             and return 1 # не проверяем доступ
212 0 0 0       unless $args->{auth} || $args->{role} || $args->{guest};
      0        
      0        
213            
214            
215 0 0         if ($args->{guest}) {# && $args->{auth} =~ m'\bguest\b'i
216 0 0 0       $app->log->debug(sprintf(qq[Access allow [%s] for {guest}],
217             $route->pattern->unparsed,
218             ))
219             and return 1
220             if $self->guest->is_guest($c);
221             }
222            
223             # не авторизовался
224             $self->deny_log($route, $args, $u, $c)
225             and $c->$fail_auth_cb()
226             and return undef
227 0 0 0       unless $u && $u->{id};
      0        
      0        
228            
229             # допустить если {auth=>'only'}
230             $app->log->debug(sprintf(qq[Access allow [%s] for {auth}=~'only'],
231             $route->pattern->unparsed,
232             ))
233             and return 1
234 0 0 0       if $args->{auth} && $args->{auth} =~ m'\bonly\b'i;
      0        
235              
236 0           my $id2 = [$u->{id}, map($_->{id}, grep !$_->{disable},@{$u->roles})];
  0            
237 0           my $id1 = [grep $_, @$args{qw(id route_id action_id controller_id namespace_id)}];
238            
239             # explicit acces to route
240 0 0 0       scalar @$id1
      0        
241             && $access->access_explicit($id1, $id2)
242             && $app->log->debug(sprintf "Access allow [%s] for roles=[%s] joined id1=%s; args=[%s]; defaults=%s",
243             $route->pattern->unparsed,
244             $c->dumper($id2) =~ s/\s+//gr,
245             $c->dumper($id1) =~ s/\s+//gr,
246             $c->dumper($args) =~ s/\s+//gr,
247             $c->dumper($route->pattern->defaults) =~ s/\s+//gr,
248             )
249             && return 1;
250            
251             # Access to non db route by role
252             $args->{role}
253             && $access->access_role($args->{role}, $id2)
254             && $app->log->debug(sprintf "Access allow [%s] by role [%s]",
255             $route->pattern->unparsed,
256             $args->{role},
257             )
258 0 0 0       && return 1;
      0        
259            
260             # implicit access to non db routes
261 0   0       my $controller = $args->{controller} || $route->pattern->defaults->{controller} && ucfirst(lc($route->pattern->defaults->{controller}));
262 0   0       my $namespace = $args->{namespace} || $route->pattern->defaults->{namespace};
263 0 0 0       if ($controller && !$namespace) {
264 0   0       (load_class(namespace=>$_, controller=>$controller) and ($namespace = $_) and last) for @{ $app->routes->namespaces };
  0   0        
265             #~ warn "FOUND CONTROLLER[$controller] in NAMESPACE: $namespace";
266             }
267            
268 0           my $fail_access_cb = $conf->{access}{fail_access_cb};
269            
270 0 0 0       $self->deny_log($route, $args, $u, $c)
      0        
      0        
271             and $c->$fail_access_cb()
272             and return undef
273             unless $controller && $namespace;# failed load class
274              
275 0 0 0       $access->access_namespace($namespace, $id2)
276             && $app->log->debug(sprintf "Access allow [%s] for roles=[%s] by namespace=[%s]; args=[%s]; defaults=[%s]",
277             $route->pattern->unparsed,
278             $c->dumper($id2) =~ s/\s+//gr,
279             $namespace,
280             $c->dumper($args) =~ s/\s+//gr,
281             $c->dumper($route->pattern->defaults) =~ s/\s+//gr,
282             )
283             && return 1;
284            
285 0 0 0       $access->access_controller($namespace, $controller, $id2)
286             && $app->log->debug(sprintf "Access allow [%s] for roles=[%s] by namespace=[%s] and controller=[%s]; args=[%s]; defaults=[%s]",
287             $route->pattern->unparsed,
288             $c->dumper($id2) =~ s/\s+//gr,
289             $namespace, $controller,
290             $c->dumper($args) =~ s/\s+//gr,
291             $c->dumper($route->pattern->defaults) =~ s/\s+//gr,
292             )
293             && return 1;
294            
295             # еще раз контроллер, который тут без namespace и в базе без namespace ------> доступ из любого места
296             $args->{namespace} || $route->pattern->defaults->{namespace}
297 0 0 0       || $access->access_controller(undef, $controller, $id2)
      0        
      0        
298             && $app->log->debug(sprintf "Access allow [%s] for roles=[%s] by controller=[%s] without namespace on db; args=[%s]; defaults=[%s]",
299             $route->pattern->unparsed,
300             $c->dumper($id2) =~ s/\s+//gr,
301             $controller,
302             $c->dumper($args) =~ s/\s+//gr,
303             $c->dumper($route->pattern->defaults) =~ s/\s+//gr,
304             )
305             && return 1;
306            
307             my $action = $args->{action} || $route->pattern->defaults->{action}
308 0 0 0       or $self->deny_log($route, $args, $u, $c)
      0        
      0        
309             and $c->$fail_access_cb()
310             and return undef;
311            
312 0 0 0       $access->access_action($namespace, $controller, $action, $id2)
313             && $app->log->debug(sprintf "Access allow [%s] for roles=[%s] by namespace=[%s] and controller=[%s] and action=[%s]; args=[%s]; defaults=[%s]",
314             $route->pattern->unparsed,
315             $c->dumper($id2) =~ s/\s+//gr,
316             $namespace , $controller, $action,
317             $c->dumper($args) =~ s/\s+//gr,
318             $c->dumper($route->pattern->defaults) =~ s/\s+//gr,
319             )
320             && return 1;
321            
322             # еще раз контроллер, который тут без namespace и в базе без namespace ------> доступ из любого места
323             $args->{namespace} || $route->pattern->defaults->{namespace}
324 0 0 0       && $access->access_action(undef, $controller, $action, $id2)
      0        
      0        
325             && $app->log->debug(sprintf "Access allow [%s] for roles=[%s] by (namespace=[any]) controller=[%s] and action=[%s]; args=[%s]; defaults=[%s]",
326             $route->pattern->unparsed,
327             $c->dumper($id2) =~ s/\s+//gr,
328             $controller, $action,
329             $c->dumper($args) =~ s/\s+//gr,
330             $c->dumper($route->pattern->defaults) =~ s/\s+//gr,
331             )
332             && return 1;
333            
334 0           $self->deny_log($route, $args, $u, $c);
335 0           $c->$fail_access_cb();
336 0           return undef;
337             }
338              
339             sub deny_log {
340 0     0 0   my $self = shift;
341 0           my ($route, $args, $u, $c) = @_;
342 0           my $app = $self->app;
343             $app->log->debug(sprintf "Access deny [%s] for profile id=[%s]; args=[%s]; defaults=[%s]",
344             $route->pattern->unparsed,
345 0 0         $u ? $u->{id} : 'non auth',
346             $app->dumper($args) =~ s/\s+//gr,
347             $app->dumper($route->pattern->defaults) =~ s/\s+//gr,
348             );
349             }
350              
351             sub model {
352 0     0 0   my ($self, $name) = @_;
353 0           my $ns = $self->merge_conf->{'model_namespace'};
354 0 0         my $class = load_class(namespace => $ns, module=> $name)
355             or die "Model module [$name] not found at namespace [$ns] or has errors";
356            
357 0           weaken $self;
358             #~ weaken $self->{app};
359 0           $class->new(app=>$self->app, plugin=>$self); # синглетоны в общем
360            
361             };
362              
363             our $VERSION = '0.872';
364              
365             =pod
366              
367             =encoding utf8
368              
369             =head1 Доброго всем
370              
371             ¡ ¡ ¡ ALL GLORY TO GLORIA ! ! !
372              
373             =head1 Mojolicious::Plugin::RoutesAuthDBI
374              
375             Plugin makes an auth operations throught the plugin L and OAuth2 by L.
376              
377             =head1 VERSION
378              
379             0.872
380              
381             =head1 NAME
382              
383             Mojolicious::Plugin::RoutesAuthDBI - from DBI tables does generate app routes, make authentication and make restrict access (authorization).
384              
385             =head1 DB DESIGN DIAGRAM
386              
387             First of all you will see L or L
388              
389             =head1 SYNOPSIS
390              
391             $app->plugin('RoutesAuthDBI',
392             dbh => $app->dbh,
393             auth => {...},
394             access => {...},
395             admin => {...},
396             oauth => {...},
397             guest => {...},
398             template => {...},
399             model_namespace=>...,
400             );
401              
402              
403             =head2 PLUGIN OPTIONS
404              
405             One option C is mandatory, all other - optional.
406              
407             =head3 dbh
408              
409             Handler DBI connection where are tables: controllers, actions, routes, logins, profiles, roles, refs and oauth.
410              
411             dbh => $app->dbh,
412             # or
413             dbh => sub { shift->dbh },
414              
415             =head3 auth
416              
417             Hashref options pass to base plugin L.
418             By default the option:
419              
420             current_user_fn => 'auth_user',
421             stash_key => "Mojolicious::Plugin::RoutesAuthDBI__user__",
422            
423             The options:
424              
425             load_user => \&load_user,
426             validate_user => \&validate_user,
427              
428             are imported from package access module. See below.
429              
430             =head3 access
431              
432             Hashref options for special access module. This module has subs/methods for manage auth and access operations, has appling routes from DBI table. By default plugin will load the builtin module:
433              
434             access => {
435             module => 'Access',
436             namespace => 'Mojolicious::Plugin::RoutesAuthDBI',
437             ...,
438             },
439              
440              
441             You might define your own module by passing options:
442              
443             access => {
444             module => 'Foo',
445             namespace => 'Bar::Baz',
446             ...,
447             },
448              
449             See L for detail options list.
450              
451             =head3 admin
452              
453             Hashref options for admin controller for actions on SQL tables routes, roles, profiles, logins. By default the builtin module:
454              
455             admin => {
456             controller => 'Admin',
457             namespace => 'Mojolicious::Plugin::RoutesAuthDBI',
458             ...,
459             },
460              
461              
462             You might define your own controller by passing options:
463              
464             admin => {
465             controller => 'Foo',
466             namespace => 'Bar::Baz',
467             ...,
468             },
469              
470             See L for detail options list.
471              
472             =head3 oauth
473              
474             Hashref options for oauth controller. By default the builtin module:
475              
476             oauth => {
477             controller => 'OAuth',
478             namespace => 'Mojolicious::Plugin::RoutesAuthDBI',
479             ...,
480             },
481              
482              
483             You might define your own controller by passing options:
484              
485             oauth => {
486             controller => 'Foo::Bar::Baz',
487             ...,
488             },
489              
490             See L for detail options list.
491              
492             =head3 guest
493              
494             Hashref options for guest module. Defaults are:
495              
496             guest => {
497             namespace => 'Mojolicious::Plugin::RoutesAuthDBI',
498             module => 'Guest',
499             session_key => 'guest_data',
500             stash_key => "Mojolicious::Plugin::RoutesAuthDBI__guest__",
501            
502             },
503              
504             Disable guest module usage:
505              
506             guest => undef, # or none in config
507              
508             See L
509              
510             =head3 model_namespace
511              
512             Where are your models place. Default to "Mojolicious::Plugin::RoutesAuthDBI::Model".
513              
514             =head3 template
515              
516             Hashref variables for SQL templates of models dictionaries. Defaults is C<$Mojolicious::Plugin::RoutesAuthDBI::Schema::defaults>. See L.
517              
518             =head1 INSTALL
519              
520             See L.
521              
522             =head1 OVER CONDITIONS
523              
524             =head2 access
525              
526             Heart of this plugin! This condition apply for all db routes even if column auth set to 0. It is possible to apply this condition to non db routes also:
527              
528             =over 4
529              
530             =item * No access check to route, but authorization by session will ready:
531              
532             $r->route('/foo')->...->over(access=>{auth=>0})->...;
533              
534             =item * Allow if has authentication only:
535              
536             $r->route('/foo')->...->over(access=>{auth=>'only'})->...;
537             # same as
538             # $r->route('/foo')->...->over(authenticated => 1)->...; # see Mojolicious::Plugin::Authentication
539              
540             =item * Allow for guest
541              
542             $r->route('/foo')->...->over(access=>{guest=>1})->...;
543            
544             To makes guest session:
545              
546             $c->access->plugin->guest->store($c, {<...some data...>});
547              
548             See L
549              
550             =item * Route accessible if profile roles assigned to either B namespace or controller 'Bar.pm' (which assigned neither namespece on db or assigned to that loadable namespace) or action 'bar' on controller Bar.pm (action record in db table actions):
551              
552             $r->route('/bar-bar-any-namespace')->to('bar#bar',)->over(access=>{auth=>1})->...;
553              
554             =item * Explicit defined namespace route accessible either namespace 'Bar' or 'Bar::Bar.pm' controller or action 'bar' in controller 'Bar::Bar.pm' (which assigned to namespace 'Bar' in table refs):
555              
556             $r->route('/bar-bar-bar')->to('bar#bar', namespace=>'Bar')->over(access=>{auth=>1})->...;
557              
558             =item * Check access by overriden namespace 'BarX': controller and action also with that namespace in db table refs:
559              
560             $r->route('/bar-nsX')->to('bar#bar', namespace=>'Bar')->over(access=>{auth=>1, namespace=>'BarX'})->...;
561              
562             =item * Check access by overriden namespace 'BarX' and controller 'BarX.pm', action record also with that ns & c in db table refs:
563              
564             $r->route('/bar-nsX-cX')->to('bar#bar', namespace=>'Bar')->over(access=>{auth=>1, namespace=>'BarX', controller=>'BarX'})->...;
565              
566             =item * Full override names access:
567              
568             $r->route('/bar-nsX-cX-aX')->to('bar#bar', namespace=>'Bar')->over(access=>{auth=>1, namespace=>'BarX', controller=>'BarX', action=>'barX'})->...;
569              
570             =item *
571              
572             $r->route('/bar-cX-aX')->to('bar#bar',)->over(access=>{auth=>1, controller=>'BarX', action=>'barX'})->...;
573              
574             =item * Route accessible if profile roles list has defined role (admin):
575              
576             $r->route('/bar-role-admin')->to('bar#bar',)->over(access=>{auth=>1, role=> 'admin'})->...;
577            
578             =item * Pass callback to access condition
579              
580             The callback will get parameters: $profile, $route, $c, $captures, $args (this callback ref). Callback must returns true or false for restrict access. Example simple auth access:
581              
582             $r->route('/check-auth')->over(access=>sub {my ($profile, $route, $c, $captures, $args) = @_; return $profile;})->to(cb=>sub {my $c =shift; $c->render(format=>'txt', text=>"Hi @{[$c->auth_user->{names}]}!\n\nYou have access!");});
583              
584             =back
585              
586             =head1 HELPERS
587              
588             =head2 access
589              
590             Returns access instance obiect. See L methods.
591              
592             if ($c->access->access_explicit([1,2,3], [1,2,3])) {
593             # yes, accessible
594             }
595              
596             =head1 METHODS and SUBS
597              
598             Registration() & access() & .
599              
600             =head2 Example routing table records
601              
602             Request
603             HTTP method(s) (optional)
604             and the URL (space delim)
605             Contoller Method Route Name Auth
606             ------------------------- ----------- -------------- ----------------- -----
607             GET /city/new City new_form city_new_form 1
608             GET /city/:id City show city_show 1
609             GET /city/edit/:id City edit_form city_edit_form 1
610             GET /cities City index city_index 1
611             POST /city City save city_save 1
612             GET /city/delete/:id City delete_form city_delete_form 1
613             DELETE /city/:id City delete city_delete 1
614             / Home index home_index 0
615             get post /foo/baz Foo baz foo_baz 1
616              
617             It table will generate the L:
618              
619             # GET /city/new
620             $r->route('/city/new')->via('get')->over()->to(controller => 'city', action => 'new_form')->name('city_new_form');
621              
622             # GET /city/123 - show item with id 123
623             $r->route('/city/:id')->via('get')->over()->to(controller => 'city', action => 'show')->name('city_show');
624              
625             # GET /city/edit/123 - form to edit an item
626             $r->route('/city/edit/:id')->via('get')->over()->to(controller => 'city', action => 'edit_form')->name('city_edit_form');
627              
628             # GET /cities - list of all items
629             $r->route('/cities')->via('get')->over()->to(controller => 'city', action => 'index')->name('cities_index');
630              
631             # POST /city - create new item or update the item
632             $r->route('/city')->via('post')->to(controller => 'city', action => 'save')->name('city_save');
633            
634             # GET /city/delete/123 - form to confirm delete an item id=123
635             $r->route('/city/delete/:id')->via('get')->over()->to(controller => 'city', action => 'delete_form')->name('city_delete_form');
636              
637             # DELETE /city/123 - delete an item id=123
638             $r->route('/city/:id')->via('delete')->over()->to(controller => 'city', action => 'delete')->name('city_delete');
639            
640             # without HTTP method and no auth restrict
641             $r->route('/')->to(controller => 'Home', action => 'index')->name('home_index');
642            
643             # GET or POST /foo/baz
644             $r->route('/foo/baz')->via('GET', 'post')->over()->to(controller => 'Foo', action => 'baz')->name('foo_baz');
645              
646             =head2 Warning
647              
648             If you changed the routes table then kill -HUP or reload app to regenerate routes. Changing assess not require reloading the service.
649              
650             =head1 SEE ALSO
651              
652             L
653              
654             L
655              
656             =head1 AUTHOR
657              
658             Михаил Че (Mikhail Che), C<< >>
659              
660             =head1 BUGS / CONTRIBUTING
661              
662             Please report any bugs or feature requests at L. Pull requests also welcome.
663              
664             =head1 COPYRIGHT
665              
666             Copyright 2016+ Mikhail Che.
667              
668             This library is free software; you can redistribute it and/or modify
669             it under the same terms as Perl itself.
670              
671             =cut
672