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   111 use Mojo::Base 'Slovo::Controller', -signatures;
  14         30  
  14         106  
3              
4 14     14   2488 use Mojo::Util qw(encode sha1_sum);
  14         34  
  14         3022  
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 3210 sub current_user_fn { return 'user' }
10              
11             # Display the form for signing in.
12             # GET /in
13 10     10 1 142 sub form ($c) {
  10         28  
  10         17  
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       55 $c->is_user_authenticated && return $c->redirect_to('/');
19 10         650 $c->stash(sign_in_error => '');
20 10         292 return $c->render;
21             }
22              
23             # Sign in the user.
24             # POST /in
25 9     9 1 120 sub sign_in ($c) {
  9         22  
  9         20  
26              
27             #1. do basic validation first
28 9         67 my $v = $c->validation;
29 9         5831 $v->required('login_name', 'trim')->like(qr/^[\p{IsAlnum}\.\-\$]{3,12}$/x);
30 9         1870 $v->required('digest')->like(qr/[0-9a-f]{40}/i);
31 9         805 $c->stash(sign_in_error => '');
32              
33 9 100       224 if ($v->csrf_protect->has_error('csrf_token')) {
    50          
34 1         43 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         287 my $o = $v->output;
49              
50             # TODO: Redirect to the page where user wanted to go or where he was before
51 8 50       112 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       32368 : 'home_upravlenie');
56 8 100       219 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 91 sub sign_out ($c) {
  5         29  
  5         18  
64 5         34 my $login_name = $c->user->{login_name};
65 5         255 $c->logout;
66 5         245 $c->app->log->info('$user ' . $login_name . ' logged out!');
67 5         199 return $c->redirect_to('authform');
68             }
69              
70 12     12 1 141 sub under_management ($c) {
  12         32  
  12         27  
71 12 100       79 unless ($c->is_user_authenticated) {
72 2         171 $c->redirect_to('authform');
73 2         1549 return 0;
74             }
75              
76 10         40417 my $uid = $c->user->{id};
77 10         361 my $path = $c->req->url->path->to_string;
78 10         911 my $route = $c->current_route;
79              
80 10 50       383 return 1 if $c->groups->is_admin($uid);
81              
82             # for now only admins can manage groups and domains
83 10 50       3224 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         634 my ($e_uid) = $path =~ m|/users/(\d+)|; #Id of the user being edited
92 10 100       47 my $e_user = $e_uid ? $c->users->find_where({id => $e_uid}) : undef;
93 10 50 66     457 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         42 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 99     99 1 5286 sub load_user ($c, $uid) {
  99         203  
  99         178  
  99         164  
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 99         444 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 231 sub validate_user ($c, $login_name, $csrf_digest, $dat) {
  8         29  
  8         18  
  8         18  
  8         19  
  8         16  
130 8         29 state $app = $c->app;
131 8         35 state $log = $app->log;
132 8         89 my $u = $c->users->find_by_login_name($login_name);
133 8 50       35722 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         788 my $csrf_token = $c->csrf_token;
139 8         375 my $checksum = sha1_sum($csrf_token . $u->{login_password});
140 8 100       47 unless ($checksum eq $csrf_digest) {
141              
142             # try the passw_login
143 1         12 my $t = time;
144             my $row = $c->dbx->db->select(
145             passw_login => 'token',
146 1         15 {start_date => {'<=' => $t}, to_uid => $u->{id}, stop_date => {'>' => $t}},
147             {-desc => ['id']})->hash;
148             my $checksum2 = sha1_sum(
149 1         3230 $csrf_token . sha1_sum(encode('UTF-8' => $u->{login_name} . $row->{token})));
150 1 50 33     105 if ($row && ($checksum2 eq $csrf_digest)) {
151 1         10 $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         1802 $app->dbx->db->delete('passw_login' => {stop_date => {'<=' => $t}});
155 1         1384 $log->info('$user ' . $u->{login_name} . ' logged in using passw_login!');
156 1         106 $c->flash(message => 'Задайте нов таен ключ!');
157 1         52 $c->stash(passw_login => 1);
158 1         24 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         70 $log->info('$user ' . $u->{login_name} . ' logged in!');
166              
167 7         103 return $u->{id};
168             }
169              
170             my $msg_expired_token = 'Връзката, която ви доведе тук, е с изтекла годност.' . '';
171              
172             # GET /first_login/
173             # GET /first_login/32e36608c72bc51c7c39a72fd7e71cba55f3e9ad
174 1     1 1 26 sub first_login_form ($c) {
  1         8  
  1         6  
175 1 50 33     19 $c->logout && $c->user($c->users->find_by_login_name('guest'))
176             if $c->is_user_authenticated;
177 1         3895 my $token = $c->param('token');
178 1         158 my $t = time;
179 1         16 my $row = $c->dbx->db->select(
180             first_login => '*',
181             {start_date => {'<=' => $t}, stop_date => {'>' => $t}, token => $token})->hash;
182 1 50       2608 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         90 return $c->render(row => $row);
189             }
190              
191             # POST /first_login
192 1     1 1 21 sub first_login ($c) {
  1         8  
  1         9  
193 1         12 state $app = $c->app;
194 1         15 my $token = $c->param('token');
195 1         633 my $t = time;
196 1         20 my $row = $c->dbx->db->select(
197             first_login => '*',
198             {start_date => {'<=' => $t}, stop_date => {'>' => $t}, token => $token})->hash;
199 1 50       2491 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         158 my $v = $c->validation;
205 1         219 $v->required('first_name', 'trim')->required('last_name', 'trim');
206 1         187 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         17 ) eq $token
214             );
215 1 50       22 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       9 if ($INC{'Slovo/Task/SendOnboardingEmail.pm'}) {
222 1         11 $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         924 $c->users->save($row->{to_uid}, {disabled => 0});
231 1         81 $c->authenticate(undef, undef, {auto_validate => $row->{to_uid}});
232 1         4152 return $c->redirect_to('edit_users' => {id => $row->{to_uid}});
233             }
234              
235             # GET /lost_password
236 2     2 1 20 sub lost_password_form ($c) {
  2         13  
  2         6  
237 2 100       15 if ($c->req->method eq 'POST') {
238 1         54 my $v = $c->validation;
239 1         514 $v->required('email', 'trim')->like(qr/^[\w\-\+\.]{1,154}\@[\w\-\+\.]{1,100}$/x);
240 1         223 my $in = $v->output;
241              
242 1 50       15 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       20 if (my $user = $c->users->find_where({email => $in->{email}})) {
247 1         747 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         928 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