Passed
Push — Auth ( 474bc2...620889 )
by Stone
02:11
created

UserModel::updateUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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