Passed
Push — Auth ( 26ff7a...6598c8 )
by Stone
01:58
created

UserModel::generatePasswordHash()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 1
dl 0
loc 23
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace App\Models;
4
5
use App\Modules\Token;
0 ignored issues
show
Bug introduced by
The type App\Modules\Token was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Core\BlogocException;
7
use Core\Constant;
8
use Core\Container;
9
use Core\Model;
10
11
class UserModel extends Model
12
{
13
14
    private $userTbl;
15
    private $roleTbl;
16
17
    /**
18
     * UserModel constructor.
19
     * @param Container $container
20
     */
21
    public function __construct(Container $container)
22
    {
23
        parent::__construct($container);
24
        $this->userTbl = $this->getTablePrefix("users");
25
        $this->roleTbl = $this->getTablePrefix("roles");
26
    }
27
28
    private function baseSqlSelect(): string
29
    {
30
        $sql = "
31
            SELECT idusers, username, avatar, email, surname, name, creation_date, last_update, locked_out, bad_login_time, bad_login_tries, role_name, role_level, reset_password_hash, reset_password_hash_generation_datetime
32
            FROM $this->userTbl
33
            INNER JOIN $this->roleTbl ON $this->userTbl.roles_idroles = $this->roleTbl.idroles 
34
        ";
35
        return $sql;
36
    }
37
38
    /**
39
     * get the password from the user email. mainly for login purposes
40
     * @param string $email
41
     * @return string
42
     * @throws BlogocException
43
     */
44
    private function getUserPassword(string $email): string
45
    {
46
        if (!$this->isEmailUsed($email)) {
47
            throw new BlogocException("Email not present in Database");
48
        }
49
        $sql = "SELECT password FROM $this->userTbl WHERE email = :email";
50
        $this->query($sql);
51
        $this->bind(':email', $email);
52
        $this->execute();
53
        return $this->stmt->fetchColumn();
54
    }
55
56
    /**
57
     * called when authentication failed
58
     * @param $user
59
     * @throws \Exception
60
     */
61
    private function addToBadLoginTries($user): void
62
    {
63
        $badLoginTries = $user->bad_login_tries + 1;
64
        $sql = "
65
            UPDATE $this->userTbl
66
            SET
67
              bad_login_time = NOW(),
68
              bad_login_tries = :badLoginTries
69
            WHERE idusers = :userId
70
        ";
71
        $this->query($sql);
72
        $this->bind(':badLoginTries', $badLoginTries);
73
        $this->bind(':userId', $user->idusers);
74
        $this->execute();
75
    }
76
77
    /**
78
     * reset the bad login count
79
     * @param $user
80
     * @throws \Exception
81
     */
82
    private function resetBadLogin($user): void
83
    {
84
        $sql = "
85
            UPDATE $this->userTbl
86
            SET
87
              bad_login_tries = 0
88
            WHERE idusers = :userId
89
        ";
90
        $this->query($sql);
91
        $this->bind(':userId', $user->idusers);
92
        $this->execute();
93
    }
94
95
    private function isAccountPasswordBlocked($user)
96
    {
97
        if ($user->bad_login_tries < Constant::NUMBER_OF_BAD_PASSWORD_TRIES) {
98
            //not enough bad tries yet
99
            return false;
100
        }
101
102
        $blockTime = strtotime($user->bad_login_time);
103
        $currentTime = time();
104
        if ($currentTime - $blockTime > Constant::LOCKOUT_MINUTES * 60) {
105
            //we have outlived the timeout, connection authorised
106
            return false;
107
        }
108
        //the account is timed out
109
        return true;
110
    }
111
112
    /**
113
     * Get all the useful data about a user from his ID
114
     * @param int $userId
115
     * @return mixed
116
     * @throws \Exception
117
     */
118
    public function getUserDetailsById(int $userId)
119
    {
120
        $sql = $this->baseSqlSelect();
121
        $sql .= "
122
            WHERE idusers = :userId
123
        ";
124
        $this->query($sql);
125
        $this->bind(':userId', $userId);
126
        $this->execute();
127
        return $this->fetch();
128
    }
129
130
    /**
131
     * get the user details from a password reset token
132
     * @param string $token
133
     * @return mixed
134
     * @throws \Exception
135
     */
136
    public function getUserDetailsByToken(string $token)
137
    {
138
        $hash = $this->generateHash($token);
139
        $sql = $this->baseSqlSelect();
140
        $sql .= "
141
            WHERE reset_password_hash = :token
142
        ";
143
        $this->query($sql);
144
        $this->bind(':token', $hash);
145
        $this->execute();
146
        $user = $this->fetch();
147
        $linkValidTime = strtotime($user->reset_password_hash_generation_datetime);
148
        $currentTime = time();
149
        if($currentTime-$linkValidTime > Constant::PASSWORD_RESET_DURATION*60)
150
        {
151
            //token is no longer valid
152
            return false;
153
        }
154
        return $user;
155
156
    }
157
158
    /**
159
     * Get all the useful data about a user from his mail
160
     * @param string $email
161
     * @return mixed
162
     * @throws BlogocException
163
     */
164
    public function getUserDetailsByEmail(string $email)
165
    {
166
        //check if email is valid for sanity
167
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
168
            $email = htmlspecialchars($email);
169
            throw new BlogocException("invalid email " . $email);
170
        }
171
        $sql = $this->baseSqlSelect();
172
        $sql .= "
173
            WHERE email = :email
174
        ";
175
        $this->query($sql);
176
        $this->bind(':email', $email);
177
        $this->execute();
178
        return $this->fetch();
179
    }
180
181
    /**
182
     * check if the email is present in the database
183
     * @param string $email
184
     * @return bool
185
     * @throws \Exception
186
     */
187
    public function isEmailUsed(string $email)
188
    {
189
        return $this->getUserDetailsByEmail($email) !== false;
190
    }
191
192
    /**
193
     * register a new user
194
     * @param \stdClass $userData
195
     * @return int
196
     * @throws \Exception
197
     */
198
    public function registerUser(\stdClass $userData): int
199
    {
200
201
        //TODO need to get the default user role. Config ??
202
        //$passwordHash = password_hash($userData->password, PASSWORD_DEFAULT);
203
204
        $sql = "
205
            INSERT INTO $this->userTbl (username, email, surname, name, creation_date, last_update, roles_idroles, locked_out, bad_login_tries)
206
            VALUES (:username, :email, :surname, :name, NOW(), NOW(), :roles_idroles, 0, 0)
207
        ";
208
        $this->query($sql);
209
        $this->bind(':username', $userData->username);
210
        $this->bind(':email', $userData->email);
211
        //$this->bind(':password', $passwordHash);
212
        $this->bind(':surname', $userData->surname);
213
        $this->bind(':name', $userData->name);
214
        $this->bind(':roles_idroles', 1);
215
        $this->execute();
216
217
        return (int)$this->dbh->lastInsertId();
218
    }
219
220
    /**
221
     * verify the user connection mail/password and login if ok
222
     * @param string $email
223
     * @param string $password
224
     * @return bool|mixed
225
     * @throws BlogocException
226
     */
227
    public function authenticateUser(string $email, string $password): \stdClass
228
    {
229
        $response = new \stdClass();
230
        $response->success = false;
231
        $response->message = "";
232
233
        $user = $this->getUserDetailsByEmail($email);
234
235
        if ($user === false) //no user exists
236
        {
237
            $response->message = "email doesn't exist, register a new account?";
238
            return $response;
239
        }
240
241
        //check if the user has validated his email
242
        if ($user->locked_out) {
243
            $response->message = "the email has not been verified, please check your inbox or click on 'reset your password'";
244
            return $response;
245
        }
246
247
        if ($this->isAccountPasswordBlocked($user)) {
248
            $response->message = "too many bad passwords, account is blocked for " . Constant::LOCKOUT_MINUTES . " minutes";
249
            return $response;
250
        }
251
252
        if (!password_verify($password, $this->getUserPassword($email))) {
253
            $response->message = "password is incorrect";
254
            $this->addToBadLoginTries($user);
255
            return $response;
256
        }
257
258
259
        //all ok, send user back for login
260
        $this->resetBadLogin($user);
261
        $response->user = $user;
262
        $response->success = true;
263
        return $response;
264
    }
265
266
    /**
267
     * generate a password hash for resetting or defining the password
268
     * @param int $userId
269
     * @return string the generated token
270
     * @throws \Exception
271
     */
272
    public function generatePasswordHash(int $userId): string
273
    {
274
        $user = $this->getUserDetailsById($userId);
275
        if (!$user) {
276
            //user Id doesn't exist, bail out
277
            throw new \Exception("User not found");
278
        }
279
        $token = $this->generateToken();
280
        $hash = $this->generateHash($token);
281
282
        $sql = "
283
            UPDATE $this->userTbl
284
            SET
285
              reset_password_hash = :hash,
286
              reset_password_hash_generation_datetime = NOW()
287
            WHERE idusers = :userId
288
        ";
289
        $this->query($sql);
290
        $this->bind(':hash', $hash);
291
        $this->bind(':userId', $user->idusers);
292
        $this->execute();
293
294
        return $token;
295
    }
296
297
    /**
298
     * Reset the user password
299
     * @param int $userId
300
     * @param string $password
301
     * @throws \Exception
302
     */
303
    public function resetPassword(int $userId, string $password)
304
    {
305
        $hash = password_hash($password, PASSWORD_DEFAULT);
306
        $sql = "
307
            UPDATE $this->userTbl
308
            SET
309
              password = :password,
310
              locked_out = 0;
311
              last_update = NOW()
312
            WHERE idusers = :userId
313
        ";
314
        $this->query($sql);
315
        $this->bind(':password', $hash);
316
        $this->bind(':userId', $userId);
317
        $this->execute();
318
    }
319
320
321
}