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