User.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <?php
  2. // Copyright 2019 Hackware SpA <human@hackware.cl>
  3. // "Hackware Web Services Core" is released under the MIT License terms.
  4. namespace Hawese\Core;
  5. use Hawese\Core\Exceptions\WrongCredentialsException;
  6. use Illuminate\Cache\RateLimiter;
  7. use RuntimeException;
  8. class User extends TableModel
  9. {
  10. public static $table = 'users';
  11. public static $attributes = [
  12. 'uid' => ['required', 'string', 'min:3', 'max:100'],
  13. 'email' => ['nullable', 'email', 'min:3', 'max:100'],
  14. 'password' => ['nullable', 'string', 'min:6'],
  15. 'display_name' => ['nullable', 'string', 'min:3', 'max:100'],
  16. 'info' => ['nullable', 'json', 'max:65535'],
  17. 'created_at' => ['nullable', 'date'],
  18. 'updated_at' => ['nullable', 'date'],
  19. 'deleted_at' => ['nullable', 'date'],
  20. ];
  21. protected $hidden = ['password', 'deleted_at'];
  22. public static $primary_key = 'uid';
  23. protected static $incrementing = false;
  24. public function changePassword(string $password): self
  25. {
  26. $this->password = password_hash($password, PASSWORD_DEFAULT);
  27. return $this;
  28. }
  29. /**
  30. * @param string $username can be the uid or email
  31. * @param string $password plain text password
  32. * @param bool $remember for automatic login ("remember me")
  33. */
  34. public static function loginByPassword(
  35. string $username,
  36. string $password,
  37. bool $remember = false
  38. ): self {
  39. self::abortIfTooManyFailedLogins($username);
  40. $user = self::find($username, ['uid', 'email']);
  41. if (!password_verify($password, $user->password)) {
  42. self::increaseFailedLogins($username);
  43. throw new WrongCredentialsException('user', $username);
  44. }
  45. self::clearFailedLogins($username);
  46. app('session')->set('user_uid', $user->uid);
  47. app('session')->migrate();
  48. if ($remember) {
  49. $user->generateHumanToken(true);
  50. }
  51. return $user;
  52. }
  53. /**
  54. * Will login based on a token key and secret.
  55. *
  56. * If Token::type is HUMAN, it will be discarded and a new one
  57. * generated on the fly as are single use tokens (i.e. "remember me"
  58. * generated tokens), unless $remember is set to false (i.e.
  59. * email tokens) check AuthServiceProvider for better understanding.
  60. */
  61. public static function loginByToken(
  62. string $key,
  63. string $secret,
  64. bool $remember = true // Only useful for HUMAN tokens
  65. ): self {
  66. self::abortIfTooManyFailedLogins($key);
  67. $token = Token::find($key);
  68. if (!password_verify($secret, $token->secret)) {
  69. self::increaseFailedLogins($user);
  70. // TODO: Notify user
  71. throw new WrongCredentialsException('token', $key);
  72. }
  73. self::clearFailedLogins($key);
  74. $user = self::find($token->user_uid);
  75. if ($token->type == Token::HUMAN) {
  76. app('session')->set('user_uid', $user->uid);
  77. $token->delete();
  78. setcookie('auth_token', '', time() - 3600, '/', null);
  79. if ($remember) {
  80. $user->generateHumanToken(true);
  81. }
  82. }
  83. return $user;
  84. }
  85. public function generateHumanToken($set_cookie = false): Token
  86. {
  87. $token = Token::generate(Token::HUMAN, $this->uid);
  88. if ($set_cookie) {
  89. setcookie(
  90. 'auth_token', // name
  91. "$token->key:$token->secret", // value
  92. time() + 30 * 24 * 60 * 60, // expires on 1 month
  93. '/', // path
  94. null, // domain origin. if set includes subdomains
  95. app()->environment() == 'production' ? true : false, // secure
  96. true, // httponly, don't allow reading through javascript
  97. );
  98. }
  99. return $token;
  100. }
  101. public function generateSystemToken(): Token
  102. {
  103. return Token::generate(Token::SYSTEM, $this->uid);
  104. }
  105. /**
  106. * @return int current hits for this $loginKey
  107. */
  108. private static function increaseFailedLogins(string $loginKey): int
  109. {
  110. // Failed logins in 1 minute
  111. return self::limiter()->hit(self::remoteId($loginKey), 60);
  112. }
  113. private static function clearFailedLogins(string $loginKey): void
  114. {
  115. self::limiter()->clear(self::remoteId($loginKey));
  116. }
  117. private static function abortIfTooManyFailedLogins(string $loginKey): void
  118. {
  119. // More than 4 attempts is too much
  120. if (self::limiter()->tooManyAttempts(self::remoteId($loginKey), 4)) {
  121. throw new RuntimeException(
  122. 'Too many failed requests. Vamo a calmarno.',
  123. 5
  124. );
  125. };
  126. }
  127. /**
  128. * Identifies a remote user
  129. * @param string $loginKey a login key value, such as `uid` or `secret`
  130. */
  131. private static function remoteId(string $loginKey): string
  132. {
  133. return strtolower($loginKey) . '|' . $_SERVER['REMOTE_ADDR'];
  134. }
  135. private static function limiter(): RateLimiter
  136. {
  137. return app(RateLimiter::class);
  138. }
  139. public function logout(): bool
  140. {
  141. if (array_key_exists('auth_token', $_COOKIE)) {
  142. list($key, $secret) = explode(':', $_COOKIE['auth_token']);
  143. Token::find($key)->delete();
  144. setcookie('auth_token', '', time() - 3600, '/', null);
  145. }
  146. return app('session')->invalidate();
  147. }
  148. /**
  149. * @param string $username can be the uid or email
  150. * @param string $origin_url $_SERVER['HTTP_REFERER'] | $_GET['origin_url']
  151. */
  152. public static function emailToken(
  153. string $username,
  154. string $origin_url
  155. ): self {
  156. self::validateOrigin($origin_url);
  157. $user = User::find($username, ['uid', 'email']);
  158. $token = $user->generateHumanToken();
  159. $email_token_link = sprintf(
  160. "%s/?auth_token=%s:%s",
  161. $origin_url,
  162. $token->key,
  163. $token->secret
  164. );
  165. self::sendEmailTokenEmail($user, $email_token_link);
  166. return $user;
  167. }
  168. private static function validateOrigin(string &$origin_url): void
  169. {
  170. $origins = config('cors.default_profile.allow_origins');
  171. if (in_array($origin_url, $origins) === false) {
  172. throw new RuntimeException('Unacceptable origin', 6);
  173. }
  174. }
  175. private static function sendEmailTokenEmail(
  176. self &$user,
  177. string &$email_token_link
  178. ): void {
  179. $mail = app(Mailer::class);
  180. $mail->addAddress($user->email, $user->display_name);
  181. $mail->Subject = __(
  182. 'Tu enlace de inicio de sesión en :app_name',
  183. ['app_name' => config('app.name')]
  184. );
  185. $mail->Body = view(
  186. 'core::emails/email_token_html',
  187. ['email_token_link' => $email_token_link]
  188. )->render();
  189. $mail->AltBody = view(
  190. 'core::emails/email_token_txt',
  191. ['email_token_link' => $email_token_link]
  192. )->render();
  193. $mail->send();
  194. }
  195. // Users own themselves
  196. public function isOwner(self $user): bool
  197. {
  198. return $this->id === $user->id;
  199. }
  200. public function isSuperUser(): bool
  201. {
  202. return $this->uid === 'hawese';
  203. }
  204. }