File Coverage

blib/lib/Slovo/Controller/Auth.pm
Criterion Covered Total %
statement 116 147 78.9
branch 29 46 63.0
condition 7 17 41.1
subroutine 12 13 92.3
pod 10 11 90.9
total 174 234 74.3


line stmt bran cond sub pod time code
1             package Slovo::Controller::Auth;
2 14     14   89 use Mojo::Base 'Slovo::Controller', -signatures;
  14         25  
  14         94  
3              
4 14     14   1719 use Mojo::Util qw(encode sha1_sum);
  14         32  
  14         2527  
5              
6             # Returns the name of the geter for the current user.
7             # Needed by Mojolicious::Plugin::Authentication.
8             # See Slovo::_before_dispatch to understand how this function name is used.
9 13     13 1 2646 sub current_user_fn { return 'user' }
10              
11             # Display the form for signing in.
12             # GET /in
13 10     10 1 109 sub form ($c) {
  10         16  
  10         18  
14              
15             #TODO: remember where the user is comming from to redirect him back
16             #afterwards if the place he is comming from in the siame domain. If not,
17             #redirect him to the main page.
18 10 50       45 $c->is_user_authenticated && return $c->redirect_to('/');
19 10         555 $c->stash(sign_in_error => '');
20 10         203 return $c->render;
21             }
22              
23             # Sign in the user.
24             # POST /in
25 9     9 1 103 sub sign_in ($c) {
  9         20  
  9         13  
26              
27             #1. do basic validation first
28 9         61 my $v = $c->validation;
29 9         4922 $v->required('login_name', 'trim')->like(qr/^[\p{IsAlnum}\.\-\$]{3,12}$/x);
30 9         1402 $v->required('digest')->like(qr/[0-9a-f]{40}/i);
31 9         638 $c->stash(sign_in_error => '');
32              
33 9 100       166 if ($v->csrf_protect->has_error('csrf_token')) {
    50          
34 1         38 return $c->render(
35             sign_in_error => 'Bad CSRF token!',
36             status => 401,
37             template => 'auth/form'
38             );
39             }
40             elsif ($v->has_error) {
41 0         0 return $c->render(
42             sign_in_error => 'И двете полета са задължителни!..',
43             status => 401,
44             template => 'auth/form'
45             );
46             }
47              
48 8         235 my $o = $v->output;
49              
50             # TODO: Redirect to the page where user wanted to go or where he was before
51 8 50       97 if ($c->authenticate($o->{login_name}, $o->{digest}, $o)) {
52             my $route
53             = ($c->stash('passw_login')
54             ? {'edit_users' => {id => $c->user->{id}}}
55 8 100       27666 : 'home_upravlenie');
56 8 100       208 return $c->redirect_to(ref($route) ? %$route : $route);
57             }
58 0         0 $c->stash(sign_in_error => 'Няма такъв потребител или ключът ви е грешен.');
59 0         0 return $c->render('auth/form');
60             }
61              
62             # GET /out
63 5     5 0 78 sub sign_out ($c) {
  5         18  
  5         15  
64 5         32 my $login_name = $c->user->{login_name};
65 5         215 $c->logout;
66 5         192 $c->app->log->info('$user ' . $login_name . ' logged out!');
67 5         182 return $c->redirect_to('authform');
68             }
69              
70 12     12 1 109 sub under_management ($c) {
  12         20  
  12         20  
71 12 100       68 unless ($c->is_user_authenticated) {
72 2         149 $c->redirect_to('authform');
73 2         1156 return 0;
74             }
75              
76 10         33492 my $uid = $c->user->{id};
77 10         268 my $path = $c->req->url->path->to_string;
78 10         799 my $route = $c->current_route;
79              
80 10 50       301 return 1 if $c->groups->is_admin($uid);
81              
82             # for now only admins can manage groups and domains
83 10 50       2715 if ($route =~ /groups|domove$/) {
84 0         0 $c->flash(message => 'Само управителите се грижат'
85             . ' за множествата от потребители и домейните.');
86 0         0 $c->redirect_to('home_upravlenie');
87 0         0 return 0;
88             }
89              
90             # only admins and users with id=created_by can change another's user account
91 10         554 my ($e_uid) = $path =~ m|/users/(\d+)|; #Id of the user being edited
92 10 100       39 my $e_user = $e_uid ? $c->users->find_where({id => $e_uid}) : undef;
93 10 50 66     363 if ( $route =~ /^(show|edit|update|remove)_users$/x
      33        
      66        
94             && $e_user
95             && ($e_user->{created_by} != $uid && $e_user->{id} != $uid))
96             {
97 0         0 $c->flash(
98             message => 'Само управителите на сметки могат да ' . 'променят чужда сметка.');
99 0         0 $c->redirect_to('home_upravlenie');
100 0         0 return 0;
101             }
102 10         38 return 1;
103             }
104              
105             # secure route /manage/minion.
106             # Allow access to only authenticated members of the admin group.
107 0     0 1 0 sub under_minion ($c) {
  0         0  
  0         0  
108              
109             # TODO: make the group configurable
110 0 0       0 unless ($c->groups->is_admin($c->user->{id})) {
111 0         0 $c->flash(message => 'Само управителите могат да управляват задачите.');
112 0         0 $c->redirect_to('home_upravlenie');
113 0         0 return 0;
114             }
115 0         0 return 1;
116             }
117              
118 85     85 1 3906 sub load_user ($c, $uid) {
  85         146  
  85         148  
  85         118  
119              
120             # TODO: implement some smarter caching (at least use Mojo::Cache).
121             # state $users ={};
122             # keys %$users >2000 && $users = {};#reset cached users.
123             # return $users->{$uid}//=$c->users->find($uid);
124 85         377 return $c->users->find($uid);
125             }
126              
127             # Used in $c->authenticate by Mojolicious::Plugin::Authentication
128             # returns the user id or nothing.
129 8     8 1 150 sub validate_user ($c, $login_name, $csrf_digest, $dat) {
  8         18  
  8         23  
  8         15  
  8         17  
  8         14  
130 8         22 state $app = $c->app;
131 8         32 state $log = $app->log;
132 8         67 my $u = $c->users->find_by_login_name($login_name);
133 8 50       29620 if (!$u) {
134 0         0 $log->error("Error signing in user [$login_name]: "
135             . "No such user or user disabled, or stop_date < now!");
136 0         0 return;
137             }
138 8         655 my $csrf_token = $c->csrf_token;
139 8         350 my $checksum = sha1_sum($csrf_token . $u->{login_password});
140 8 100       34 unless ($checksum eq $csrf_digest) {
141              
142             # try the passw_login
143 1         7 my $t = time;
144             my $row = $c->dbx->db->select(
145             passw_login => 'token',
146 1         16 {start_date => {'<=' => $t}, to_uid => $u->{id}, stop_date => {'>' => $t}},
147             {-desc => ['id']})->hash;
148             my $checksum2 = sha1_sum(
149 1         2706 $csrf_token . sha1_sum(encode('UTF-8' => $u->{login_name} . $row->{token})));
150 1 50 33     82 if ($row && ($checksum2 eq $csrf_digest)) {
151 1         13 $app->dbx->db->delete('passw_login' => {to_uid => $u->{id}});
152              
153             # also delete expired but not deleted (for any reason) login tokens.
154 1         1389 $app->dbx->db->delete('passw_login' => {stop_date => {'<=' => $t}});
155 1         1079 $log->info('$user ' . $u->{login_name} . ' logged in using passw_login!');
156 1         68 $c->flash(message => 'Задайте нов таен ключ!');
157 1         49 $c->stash(passw_login => 1);
158 1         20 return $u->{id};
159             }
160 0         0 $log->error("Error signing in user [$u->{login_name}]:"
161             . "\$csrf_token:$csrf_token|\$checksum:$checksum \$csrf_digest:$csrf_digest)");
162 0         0 $log->error('$checksum:sha1_sum($csrf_token . $u->{login_password}) ne $csrf_digest');
163 0         0 return;
164             }
165 7         68 $log->info('$user ' . $u->{login_name} . ' logged in!');
166              
167 7         100 return $u->{id};
168             }
169              
170             my $msg_expired_token = 'Връзката, която ви доведе тук, е с изтекла годност.' . '';
171              
172             # GET /first_login/
173             # GET /first_login/32e36608c72bc51c7c39a72fd7e71cba55f3e9ad
174 1     1 1 31 sub first_login_form ($c) {
  1         12  
  1         4  
175 1 50 33     11 $c->logout && $c->user($c->users->find_by_login_name('guest'))
176             if $c->is_user_authenticated;
177 1         3211 my $token = $c->param('token');
178 1         113 my $t = time;
179 1         14 my $row = $c->dbx->db->select(
180             first_login => '*',
181             {start_date => {'<=' => $t}, stop_date => {'>' => $t}, token => $token})->hash;
182 1 50       2191 unless (defined $row) {
183 0         0 $c->stash('error_message' => $msg_expired_token);
184 0   0     0 $c->app->log->error('Token for first_login_form was not found for user comming from '
185             . ($c->req->headers->referrer || 'nowhere')
186             . '.');
187             }
188 1         77 return $c->render(row => $row);
189             }
190              
191             # POST /first_login
192 1     1 1 16 sub first_login ($c) {
  1         6  
  1         7  
193 1         6 state $app = $c->app;
194 1         15 my $token = $c->param('token');
195 1         454 my $t = time;
196 1         12 my $row = $c->dbx->db->select(
197             first_login => '*',
198             {start_date => {'<=' => $t}, stop_date => {'>' => $t}, token => $token})->hash;
199 1 50       2099 unless (defined $row) {
200 0         0 $c->stash(error_message => $msg_expired_token);
201 0         0 return $c->render('auth/first_login_form');
202              
203             }
204 1         121 my $v = $c->validation;
205 1         171 $v->required('first_name', 'trim')->required('last_name', 'trim');
206 1         144 my $in = $v->output;
207             my $ok = (
208             sha1_sum(
209             $row->{start_date}
210             . encode('UTF-8' => $in->{first_name} . $in->{last_name})
211             . $row->{from_uid}
212             . $row->{to_uid}
213 1         13 ) eq $token
214             );
215 1 50       19 unless ($ok) {
216 0         0 $c->stash(error_message => 'Моля, въведете имената на човека,'
217             . ' създал вашата сметка, както са изписани в'
218             . ' електроннто съобщение с препратката за първо влизане.');
219 0         0 return $c->render(template => 'auth/first_login_form', row => $row);
220             }
221 1 50       16 if ($INC{'Slovo/Task/SendOnboardingEmail.pm'}) {
222 1         12 $app->minion->enqueue(delete_first_login => [$row->{to_uid}, $token]);
223             }
224             else {
225 0         0 $app->dbx->db->delete('first_login' => {token => $token, user_id => $row->{to_uid}});
226              
227             # also delete expired but not deleted (for any reason) login tokens.
228 0         0 $app->dbx->db->delete('first_login' => {stop_date => {'<=' => time}});
229             }
230 1         739 $c->users->save($row->{to_uid}, {disabled => 0});
231 1         228 $c->authenticate(undef, undef, {auto_validate => $row->{to_uid}});
232 1         3273 return $c->redirect_to('edit_users' => {id => $row->{to_uid}});
233             }
234              
235             # GET /lost_password
236 2     2 1 17 sub lost_password_form ($c) {
  2         7  
  2         5  
237 2 100       12 if ($c->req->method eq 'POST') {
238 1         27 my $v = $c->validation;
239 1         406 $v->required('email', 'trim')->like(qr/^[\w\-\+\.]{1,154}\@[\w\-\+\.]{1,100}$/x);
240 1         170 my $in = $v->output;
241              
242 1 50       10 if ($INC{'Slovo/Task/SendPasswEmail.pm'}) {
243              
244             # send email to the user to login with a temporary password and change his
245             # password.
246 1 50       17 if (my $user = $c->users->find_where({email => $in->{email}})) {
247 1         648 my $job_id
248             = $c->minion->enqueue(mail_passw_login => [$user, $c->req->headers->host]);
249             }
250             else {
251 0         0 $c->app->log->warn('User not found by email to send temporary login password.');
252             }
253             }
254             }
255 2         710 return $c->render();
256              
257             }
258              
259             1;
260              
261             =encoding utf8
262              
263             =head1 NAME
264              
265             Slovo::Controller::Auth - и миръ Его не позна.
266              
267             =head1 DESCRIPTION
268              
269             L implements actions for authenticating users. It
270             depends on functionality, provided by L.
271             All the routes' paths mentioned below are easily modifiable because they are
272             described in C thanks to
273             L.
274              
275             =head1 ACTIONS
276              
277             Mojolicious::Plugin::Authentication implements the following actions.
278              
279             =head2 first_login_form
280              
281             Displays a form for confirmation of the names of the user who invited the new
282             user.
283              
284             GET /first_login/
285              
286             C is a route type matching C.
287              
288             =head2 first_login
289              
290             Compares the entered names of the inviting user with the token and makes other
291             checks. Signs in the user for the first time.
292              
293             =head2 form
294              
295             Route: C<{get =E '/in', to =E 'auth#form', name =E 'authform'}>.
296              
297             Renders a login form. The password is never transmitted in plain text. A digest
298             is prepared in the browser using JavaScript (see
299             C). The digest is sent and
300             compared on the server side. The digest is different in every POST request.
301              
302             =head2 lost_password_form
303              
304             Route:
305              
306             {
307             any => '/lost_password',
308             to => 'auth#lost_password_form',
309             name => 'lost_password_form'
310             },
311              
312             In case the request is not C C<$c-Eurl_for('lost_password_form')> displays a form
313             for entering email to which a temporary password to be send. If the request
314             method is C, enqueues L, if a
315             user with the given email is found in the database.
316              
317             =head2 sign_in
318              
319             Route: C<{post =E '/in', to =E 'auth#sign_in', name =E 'sign_in'}>.
320              
321             Finds and logs in a user locally. On success redirects the user to
322             L. On failure redirects
323             again to the login page.
324              
325             =head2 under_management
326              
327             This is a callback when user tries to access a page I C. If
328             user is authenticated returns true. If not, returns false and redirects to
329             L.
330              
331             =head2 under_minion
332              
333              
334             Allow access to only authenticated members of the admin group. All routes
335             generated by L are under this route.
336              
337             GET /manage/minion
338              
339              
340             =head2 METHODS
341              
342             L inherits all methods from L and
343             implements the following new ones.
344              
345             =head1 FUNCTIONS
346              
347             Slovo::Controller::Auth implements the following functions executed or used by
348             L.
349              
350             =head2 current_user_fn
351              
352             Returns the name of the helper used for getting the properties of the current
353             user. The name is C. It is passed in configuration to
354             L to generate the helper with this name.
355             This value must not be changed. Otherwise you will get runtime errors all over
356             the place because C<$c-Euser> is used a lot.
357              
358             =head2 load_user
359              
360             This function is passed to L as reference.
361             See L.
362              
363              
364             =head2 validate_user
365              
366             This function is passed to L as reference.
367             See L.
368              
369              
370             =head1 SEE ALSO
371              
372             L,
373             L
374              
375             =cut