File Coverage

lib/Slovo/Controller/Auth.pm
Criterion Covered Total %
statement 122 147 82.9
branch 32 46 69.5
condition 9 17 52.9
subroutine 12 13 92.3
pod 10 11 90.9
total 185 234 79.0


line stmt bran cond sub pod time code
1             package Slovo::Controller::Auth;
2 14     14   113 use Mojo::Base 'Slovo::Controller', -signatures;
  14         32  
  14         86  
3              
4 14     14   3217 use Mojo::Util qw(encode sha1_sum);
  14         36  
  14         2822  
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 14     14 1 42523 sub current_user_fn { return 'user' }
10              
11             # Display the form for signing in.
12             # GET /in
13 14     14 1 166 sub form ($c) {
  14         28  
  14         22  
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 14 50       74 $c->is_user_authenticated && return $c->redirect_to('/');
19 14         796 $c->stash(sign_in_error => '');
20 14         284 return $c->render;
21             }
22              
23             # Sign in the user.
24             # POST /in
25 12     12 1 134 sub sign_in ($c) {
  12         25  
  12         25  
26              
27             #1. do basic validation first
28 12         89 my $v = $c->validation;
29 12         6811 $v->required('login_name', 'trim')->like(qr/^[\p{IsAlnum}\.\-\$]{3,12}$/x);
30 12         1955 $v->required('digest')->like(qr/[0-9a-f]{40}/i);
31 12         855 $c->stash(sign_in_error => '');
32              
33 12 100       245 if ($v->csrf_protect->has_error('csrf_token')) {
    50          
34 1         42 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 11         353 my $o = $v->output;
49              
50             # TODO: Redirect to the page where user wanted to go or where he was before
51 11 50       134 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 11 100       38586 : 'home_upravlenie');
56 11 100       263 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 7     7 0 105 sub sign_out ($c) {
  7         22  
  7         13  
64 7         37 my $login_name = $c->user->{login_name};
65 7         337 $c->logout;
66 7         280 $c->app->log->info('$user ' . $login_name . ' logged out!');
67 7         250 return $c->redirect_to('authform');
68             }
69              
70 65     65 1 567 sub under_management ($c) {
  65         99  
  65         90  
71 65 100       308 unless ($c->is_user_authenticated) {
72 3         202 $c->redirect_to('authform');
73 3         1781 return 0;
74             }
75              
76 62         217589 my $uid = $c->user->{id};
77 62         1724 my $path = $c->req->url->path->to_string;
78 62         5683 my $route = $c->current_route;
79              
80 62 100       1993 return 1 if $c->groups->is_admin($uid);
81              
82             # for now only admins can manage groups and domains
83 24 100       6683 if ($route =~ /groups|domove$/) {
84 1         53 $c->flash(message => 'Само управителите се грижат'
85             . ' за множествата от потребители и домейните.');
86 1         38 $c->redirect_to('home_upravlenie');
87 1         804 return 0;
88             }
89              
90             # only admins and users with id=created_by can change another's user account
91 23         1290 my ($e_uid) = $path =~ m|/users/(\d+)|; #Id of the user being edited
92 23 100       108 my $e_user = $e_uid ? $c->users->find_where({id => $e_uid}) : undef;
93 23 100 66     1011 if ( $route =~ /^(show|edit|update|remove)_users$/x
      66        
      100        
94             && $e_user
95             && ($e_user->{created_by} != $uid && $e_user->{id} != $uid))
96             {
97 1         12 $c->flash(
98             message => 'Само управителите на сметки могат да ' . 'променят чужда сметка.');
99 1         43 $c->redirect_to('home_upravlenie');
100 1         865 return 0;
101             }
102 22         92 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 196     196 1 8525 sub load_user ($c, $uid) {
  196         338  
  196         286  
  196         256  
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 196         846 return $c->users->find($uid);
125             }
126              
127             # Used in $c->authenticate by Mojolicious::Plugin::Authentication
128             # returns the user id or nothing.
129 11     11 1 237 sub validate_user ($c, $login_name, $csrf_digest, $dat) {
  11         23  
  11         21  
  11         18  
  11         21  
  11         19  
130 11         29 state $app = $c->app;
131 11         47 state $log = $app->log;
132 11         85 my $u = $c->users->find_by_login_name($login_name);
133 11 50       41650 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 11         968 my $csrf_token = $c->csrf_token;
139 11         514 my $checksum = sha1_sum($csrf_token . $u->{login_password});
140 11 100       58 unless ($checksum eq $csrf_digest) {
141              
142             # try the passw_login
143 1         15 my $t = time;
144             my $row = $c->dbx->db->select(
145             passw_login => 'token',
146 1         11 {start_date => {'<=' => $t}, to_uid => $u->{id}, stop_date => {'>' => $t}},
147             {-desc => ['id']})->hash;
148             my $checksum2 = sha1_sum(
149 1         2698 $csrf_token . sha1_sum(encode('UTF-8' => $u->{login_name} . $row->{token})));
150 1 50 33     94 if ($row && ($checksum2 eq $csrf_digest)) {
151 1         12 $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         1381 $app->dbx->db->delete('passw_login' => {stop_date => {'<=' => $t}});
155 1         1127 $log->info('$user ' . $u->{login_name} . ' logged in using passw_login!');
156 1         79 $c->flash(message => 'Задайте нов таен ключ!');
157 1         49 $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 10         95 $log->info('$user ' . $u->{login_name} . ' logged in!');
166              
167 10         156 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         6  
  1         6  
175 1 50 33     10 $c->logout && $c->user($c->users->find_by_login_name('guest'))
176             if $c->is_user_authenticated;
177 1         3281 my $token = $c->param('token');
178 1         105 my $t = time;
179 1         12 my $row = $c->dbx->db->select(
180             first_login => '*',
181             {start_date => {'<=' => $t}, stop_date => {'>' => $t}, token => $token})->hash;
182 1 50       2102 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         61 return $c->render(row => $row);
189             }
190              
191             # POST /first_login
192 1     1 1 16 sub first_login ($c) {
  1         3  
  1         5  
193 1         11 state $app = $c->app;
194 1         12 my $token = $c->param('token');
195 1         470 my $t = time;
196 1         5 my $row = $c->dbx->db->select(
197             first_login => '*',
198             {start_date => {'<=' => $t}, stop_date => {'>' => $t}, token => $token})->hash;
199 1 50       2026 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         63 my $v = $c->validation;
205 1         171 $v->required('first_name', 'trim')->required('last_name', 'trim');
206 1         146 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       18 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         15 $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         85 $c->authenticate(undef, undef, {auto_validate => $row->{to_uid}});
232 1         3242 return $c->redirect_to('edit_users' => {id => $row->{to_uid}});
233             }
234              
235             # GET /lost_password
236 2     2 1 21 sub lost_password_form ($c) {
  2         13  
  2         12  
237 2 100       9 if ($c->req->method eq 'POST') {
238 1         19 my $v = $c->validation;
239 1         409 $v->required('email', 'trim')->like(qr/^[\w\-\+\.]{1,154}\@[\w\-\+\.]{1,100}$/x);
240 1         171 my $in = $v->output;
241              
242 1 50       12 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       13 if (my $user = $c->users->find_where({email => $in->{email}})) {
247 1         603 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         744 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