Passed
Push — Auth ( c6ffd2...76831f )
by Stone
02:02
created

UserModel::rememberMe()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 1
dl 0
loc 17
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
    private $rememberedLoginTbl;
17
18
    private $token;
19
20
    public function __construct(Container $container)
21
    {
22
        parent::__construct($container);
23
        $this->userTbl = $this->getTablePrefix("users");
24
        $this->roleTbl = $this->getTablePrefix("roles");
25
        $this->rememberedLoginTbl = $this->getTablePrefix("remembered_logins");
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
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 all the useful data about a user from his mail
132
     * @param string $email
133
     * @return mixed
134
     * @throws BlogocException
135
     */
136
    public function getUserDetailsByEmail(string $email)
137
    {
138
        //check if email is valid for sanity
139
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
140
            $email = htmlspecialchars($email);
141
            throw new BlogocException("invalid email " . $email);
142
        }
143
        $sql = $this->baseSqlSelect();
144
        $sql .= "
145
            WHERE email = :email
146
        ";
147
        $this->query($sql);
148
        $this->bind(':email', $email);
149
        $this->execute();
150
        return $this->fetch();
151
    }
152
153
    /**
154
     * check if the email is present in the database
155
     * @param string $email
156
     * @return bool
157
     * @throws \Exception
158
     */
159
    public function isEmailUsed(string $email)
160
    {
161
        return $this->getUserDetailsByEmail($email) !== false;
162
    }
163
164
    /**
165
     * register a new user
166
     * @param \stdClass $userData
167
     * @return int
168
     * @throws \Exception
169
     */
170
    public function registerUser(\stdClass $userData): int
171
    {
172
173
        //TODO need to get the default user role. Config ??
174
        $passwordHash = password_hash($userData->password, PASSWORD_DEFAULT);
175
176
        $sql = "
177
            INSERT INTO $this->userTbl (username, email, password, surname, name, creation_date, last_update, roles_idroles, locked_out, bad_login_tries)
178
            VALUES (:username, :email, :password, :surname, :name, NOW(), NOW(), :roles_idroles, 0, 0)
179
        ";
180
        $this->query($sql);
181
        $this->bind(':username', $userData->username);
182
        $this->bind(':email', $userData->email);
183
        $this->bind(':password', $passwordHash);
184
        $this->bind(':surname', $userData->surname);
185
        $this->bind(':name', $userData->name);
186
        $this->bind(':roles_idroles', 1);
187
        $this->execute();
188
189
        return (int)$this->dbh->lastInsertId();
190
    }
191
192
    /**
193
     * verify the user connection mail/password and login if ok
194
     * @param string $email
195
     * @param string $password
196
     * @return bool|mixed
197
     * @throws BlogocException
198
     */
199
    public function authenticateUser(string $email, string $password): \stdClass
200
    {
201
        $response = new \stdClass();
202
        $response->success = false;
203
        $response->message = "";
204
205
        $user = $this->getUserDetailsByEmail($email);
206
207
        if ($user === false) //no user exists
208
        {
209
            $response->message = "email doesn't exist, register a new account?";
210
            return $response;
211
        }
212
213
        //check if the user has validated his email
214
        if ($user->locked_out) {
215
            $response->message = "the email has not been verified, please check your inbox or click on 'reset your password'";
216
            return $response;
217
        }
218
219
        if ($this->isAccountPasswordBlocked($user)) {
220
            $response->message = "too many bad passwords, account is blocked for " . Constant::LOCKOUT_MINUTES . " minutes";
221
            return $response;
222
        }
223
224
        if (!password_verify($password, $this->getUserPassword($email))) {
225
            $response->message = "password is incorrect";
226
            $this->addToBadLoginTries($user);
227
            return $response;
228
        }
229
230
231
        //all ok, send user back for login
232
        $this->resetBadLogin($user);
233
        $response->user = $user;
234
        $response->success = true;
235
        return $response;
236
    }
237
238
239
    public function setToken(string $token_value = null)
240
    {
241
        if ($token_value) {
242
            $this->token = $token_value;
243
        } else {
244
            $this->token = bin2hex(random_bytes(16));
245
        }
246
    }
247
248
    /**
249
     * get the generated token
250
     * @return string
251
     */
252
    public function getToken()
253
    {
254
        return $this->token;
255
    }
256
257
    /**
258
     * get the token hash from a user id
259
     * @param int $userId
260
     * @return mixed
261
     * @throws \Exception
262
     */
263
    public function getTokenHashFromId(int $userId)
264
    {
265
        $sql = "
266
            SELECT token_hash FROM $this->rememberedLoginTbl
267
            WHERE users_idusers = :userId
268
        ";
269
        $this->query($sql);
270
        $this->bind(':userId', $userId);
271
        $this->execute();
272
        return $this->stmt->fetchColumn();
273
    }
274
275
    /**
276
     * get the hash of the token
277
     * @return string
278
     */
279
    public function getHash()
280
    {
281
        return hash_hmac("sha256", $this->token, Constant::HASH_KEY);
282
    }
283
284
    /**
285
     * Find a user session token in the database
286
     * @param string $token
287
     * @return mixed
288
     * @throws \Exception
289
     */
290
    public function findByToken(string $token)
291
    {
292
        $this->setToken($token);
293
        $hashedToken = $this->getHash();
294
295
        $sql = "
296
            SELECT * FROM $this->rememberedLoginTbl
297
            WHERE token_hash = :hashedToken
298
        ";
299
        $this->query($sql);
300
        $this->bind(':hashedToken', $hashedToken);
301
        $this->execute();
302
        $result = $this->fetch();
303
304
        if($result)
305
        {
306
            if(strtotime($result->expires_at) < time())
307
            {
308
                //token has expired
309
                $this->deleteToken($hashedToken);
310
                return false;
311
            }
312
        }
313
        
314
        return $result;
315
316
    }
317
318
    /**
319
     * Add a remember me token we store the token and use the hash in the database
320
     * @param int $userId
321
     * @return bool
322
     * @throws \Exception
323
     */
324
    public function rememberMe(int $userId):\stdClass
325
    {
326
        $result = new \stdClass();
327
        $result->token = $this->getToken();
328
        $tokenHash = $this->getHash();
329
        $result->expiry_timestamp = time() + 60 * 60 * 24 * 30; //expires in 30 days
330
331
        $sql = "
332
            INSERT INTO $this->rememberedLoginTbl (token_hash, users_idusers, expires_at)
333
            VALUES (:hashedToken, :userId, :expiresAt)
334
        ";
335
        $this->query($sql);
336
        $this->bind(':hashedToken', $tokenHash);
337
        $this->bind(':userId', $userId);
338
        $this->bind(':expiresAt', date('Y-m-d H:i:s', $result->expiry_timestamp));
339
        $result->success = $this->execute();
340
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type stdClass which is incompatible with the documented return type boolean.
Loading history...
341
342
    }
343
344
    /**
345
     * delete a token from database
346
     * @param $tokenHash
347
     * @throws \Exception
348
     */
349
    public function deleteToken($tokenHash)
350
    {
351
        $sql = "
352
            DELETE FROM $this->rememberedLoginTbl
353
            WHERE token_hash = :hashedToken;
354
        ";
355
        $this->query($sql);
356
        $this->bind(':hashedToken', $tokenHash);
357
        $this->execute();
358
359
    }
360
}