123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- <?php
- // Copyright 2019 Hackware SpA <human@hackware.cl>
- // "Hackware Web Services Core" is released under the MIT License terms.
- namespace Hawese\Core;
- use Hawese\Core\Exceptions\WrongCredentialsException;
- use Illuminate\Cache\RateLimiter;
- use RuntimeException;
- class User extends TableModel
- {
- public static $table = 'users';
- public static $attributes = [
- 'uid' => ['required', 'string', 'min:3', 'max:100'],
- 'email' => ['nullable', 'email', 'min:3', 'max:100'],
- 'password' => ['nullable', 'string', 'min:6'],
- 'display_name' => ['nullable', 'string', 'min:3', 'max:100'],
- 'info' => ['nullable', 'json', 'max:65535'],
- 'created_at' => ['nullable', 'date'],
- 'updated_at' => ['nullable', 'date'],
- 'deleted_at' => ['nullable', 'date'],
- ];
- protected $hidden = ['password', 'deleted_at'];
- public static $primary_key = 'uid';
- protected static $incrementing = false;
- public function changePassword(string $password): self
- {
- $this->password = password_hash($password, PASSWORD_DEFAULT);
- return $this;
- }
- /**
- * @param string $username can be the uid or email
- * @param string $password plain text password
- * @param bool $remember for automatic login ("remember me")
- */
- public static function loginByPassword(
- string $username,
- string $password,
- bool $remember = false
- ): self {
- self::abortIfTooManyFailedLogins($username);
- $user = self::find($username, ['uid', 'email']);
- if (!password_verify($password, $user->password)) {
- self::increaseFailedLogins($username);
- throw new WrongCredentialsException('user', $username);
- }
- self::clearFailedLogins($username);
- app('session')->set('user_uid', $user->uid);
- app('session')->migrate();
- if ($remember) {
- $user->generateHumanToken(true);
- }
- return $user;
- }
- /**
- * Will login based on a token key and secret.
- *
- * If Token::type is HUMAN, it will be discarded and a new one
- * generated on the fly as are single use tokens (i.e. "remember me"
- * generated tokens), unless $remember is set to false (i.e.
- * email tokens) check AuthServiceProvider for better understanding.
- */
- public static function loginByToken(
- string $key,
- string $secret,
- bool $remember = true // Only useful for HUMAN tokens
- ): self {
- self::abortIfTooManyFailedLogins($key);
- $token = Token::find($key);
- if (!password_verify($secret, $token->secret)) {
- self::increaseFailedLogins($user);
- // TODO: Notify user
- throw new WrongCredentialsException('token', $key);
- }
- self::clearFailedLogins($key);
- $user = self::find($token->user_uid);
- if ($token->type == Token::HUMAN) {
- app('session')->set('user_uid', $user->uid);
- $token->delete();
- setcookie('auth_token', '', time() - 3600, '/', null);
- if ($remember) {
- $user->generateHumanToken(true);
- }
- }
- return $user;
- }
- public function generateHumanToken($set_cookie = false): Token
- {
- $token = Token::generate(Token::HUMAN, $this->uid);
- if ($set_cookie) {
- setcookie(
- 'auth_token', // name
- "$token->key:$token->secret", // value
- time() + 30 * 24 * 60 * 60, // expires on 1 month
- '/', // path
- null, // domain origin. if set includes subdomains
- app()->environment() == 'production' ? true : false, // secure
- true, // httponly, don't allow reading through javascript
- );
- }
- return $token;
- }
-
- public function generateSystemToken(): Token
- {
- return Token::generate(Token::SYSTEM, $this->uid);
- }
- /**
- * @return int current hits for this $loginKey
- */
- private static function increaseFailedLogins(string $loginKey): int
- {
- // Failed logins in 1 minute
- return self::limiter()->hit(self::remoteId($loginKey), 60);
- }
- private static function clearFailedLogins(string $loginKey): void
- {
- self::limiter()->clear(self::remoteId($loginKey));
- }
- private static function abortIfTooManyFailedLogins(string $loginKey): void
- {
- // More than 4 attempts is too much
- if (self::limiter()->tooManyAttempts(self::remoteId($loginKey), 4)) {
- throw new RuntimeException(
- 'Too many failed requests. Vamo a calmarno.',
- 5
- );
- };
- }
- /**
- * Identifies a remote user
- * @param string $loginKey a login key value, such as `uid` or `secret`
- */
- private static function remoteId(string $loginKey): string
- {
- return strtolower($loginKey) . '|' . $_SERVER['REMOTE_ADDR'];
- }
- private static function limiter(): RateLimiter
- {
- return app(RateLimiter::class);
- }
- public function logout(): bool
- {
- if (array_key_exists('auth_token', $_COOKIE)) {
- list($key, $secret) = explode(':', $_COOKIE['auth_token']);
- Token::find($key)->delete();
- setcookie('auth_token', '', time() - 3600, '/', null);
- }
- return app('session')->invalidate();
- }
-
- /**
- * @param string $username can be the uid or email
- * @param string $origin_url $_SERVER['HTTP_REFERER'] | $_GET['origin_url']
- */
- public static function emailToken(
- string $username,
- string $origin_url
- ): self {
- self::validateOrigin($origin_url);
- $user = User::find($username, ['uid', 'email']);
- $token = $user->generateHumanToken();
- $email_token_link = sprintf(
- "%s/?auth_token=%s:%s",
- $origin_url,
- $token->key,
- $token->secret
- );
- self::sendEmailTokenEmail($user, $email_token_link);
- return $user;
- }
- private static function validateOrigin(string &$origin_url): void
- {
- $origins = config('cors.default_profile.allow_origins');
- if (in_array($origin_url, $origins) === false) {
- throw new RuntimeException('Unacceptable origin', 6);
- }
- }
- private static function sendEmailTokenEmail(
- self &$user,
- string &$email_token_link
- ): void {
- $mail = app(Mailer::class);
- $mail->addAddress($user->email, $user->display_name);
- $mail->Subject = __(
- 'Tu enlace de inicio de sesión en :app_name',
- ['app_name' => config('app.name')]
- );
- $mail->Body = view(
- 'core::emails/email_token_html',
- ['email_token_link' => $email_token_link]
- )->render();
- $mail->AltBody = view(
- 'core::emails/email_token_txt',
- ['email_token_link' => $email_token_link]
- )->render();
- $mail->send();
- }
- // Users own themselves
- public function isOwner(self $user): bool
- {
- return $this->id === $user->id;
- }
- public function isSuperUser(): bool
- {
- return $this->uid === 'hawese';
- }
- }
|