the browser-facing portion of osu!
at master 136 lines 3.6 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6declare(strict_types=1); 7 8namespace App\Libraries\User; 9 10use App\Mail\PasswordReset; 11use App\Models\User; 12 13class PasswordResetData 14{ 15 private const RESEND_MAIL_INTERVAL = 300; 16 17 private string $cacheKey; 18 19 private function __construct( 20 public array $attrs, 21 private readonly User $user, 22 string $username 23 ) { 24 $this->cacheKey = static::cacheKey($this->user, $username); 25 } 26 27 public static function create(?User $user, ?string $username): ?string 28 { 29 if ($user === null || $username === null) { 30 return osu_trans('password_reset.error.user_not_found'); 31 } 32 33 if (static::find($user, $username) !== null) { 34 return null; 35 } 36 37 if (!is_valid_email_format($user->user_email)) { 38 return osu_trans('password_reset.error.contact_support'); 39 } 40 41 if ($user->isPrivileged() && $user->user_password !== '') { 42 return osu_trans('password_reset.error.is_privileged'); 43 } 44 45 $now = time(); 46 $data = new static([ 47 'authHash' => static::authHash($user), 48 'canResendMailAfter' => $now - 1, 49 'key' => bin2hex(random_bytes($GLOBALS['cfg']['osu']['user']['password_reset']['key_length'] / 2)), 50 'expiresAt' => $now + $GLOBALS['cfg']['osu']['user']['password_reset']['expires_hour'] * 3600, 51 'tries' => 0, 52 ], $user, $username); 53 $data->sendMail(); 54 $data->save(); 55 56 return null; 57 } 58 59 public static function find(User $user, string $username): ?static 60 { 61 if ($user === null) { 62 return null; 63 } 64 65 $attrs = \Cache::get(static::cacheKey($user, $username)); 66 67 if ($attrs === null) { 68 return null; 69 } 70 71 return new static($attrs, $user, $username); 72 } 73 74 public static function cacheKey(User $user, string $username): string 75 { 76 $type = strpos($username, '@') === false 77 ? 'username' 78 : 'email'; 79 80 return "password_reset:data:{$user->getKey()}:{$type}"; 81 } 82 83 private static function authHash(User $user): string 84 { 85 return hash('sha256', $user->user_email ?? '').':'.hash('sha256', $user->user_password); 86 } 87 88 public function delete(): void 89 { 90 \Cache::delete($this->cacheKey); 91 } 92 93 public function hasMoreTries(): bool 94 { 95 return $this->attrs['tries'] < $GLOBALS['cfg']['osu']['user']['password_reset']['tries']; 96 } 97 98 public function isActive(): bool 99 { 100 return $this->attrs['expiresAt'] > time() 101 && hash_equals($this->attrs['authHash'], static::authHash($this->user)); 102 } 103 104 public function isValidKey(string $key): bool 105 { 106 $isValid = hash_equals($this->attrs['key'], $key); 107 108 if (!$isValid) { 109 $this->attrs['tries']++; 110 } 111 112 return $isValid; 113 } 114 115 public function save(): void 116 { 117 \Cache::put($this->cacheKey, $this->attrs, $this->attrs['expiresAt'] - time()); 118 } 119 120 public function sendMail(): bool 121 { 122 $now = time(); 123 124 if ($now < $this->attrs['canResendMailAfter']) { 125 return false; 126 } 127 128 \Mail::to($this->user)->send(new PasswordReset([ 129 'user' => $this->user, 130 'key' => $this->attrs['key'], 131 ])); 132 $this->attrs['canResendMailAfter'] = $now + static::RESEND_MAIL_INTERVAL; 133 134 return true; 135 } 136}