first(); if (! $user) { return; } $token = Str::random(64); $user->reset_password_token = $this->digest($token); $user->reset_password_sent_at = now(); $user->save(); $locale = in_array($lang, self::MAIL_LOCALES, true) ? $lang : 'en'; $url = rtrim(config('mint.server.dashboard_v6_base_path'), '/').'/anonymous/reset-password/'.$token; try { Mail::to($user->email)->send(new PasswordResetMail($url, $locale)); } catch (Throwable $e) { Log::error('v3 password reset mail failed', ['message' => $e->getMessage()]); // 用 BusinessException 而不是让它冒成 500:兜底处理器会抹掉 5xx 的 detail, // 而这条文案得让用户看到(稍后重试,而不是干等一封不会来的邮件) throw new BusinessException(__('messages.mail_send_failed'), 503, 'mail-send-failed'); } } /** * 凭 token 找到待重置的用户,token 不存在或已过期时 404。 * * @return array{username: string, expires_at: CarbonInterface} */ public function find(string $token): array { $user = $this->userByToken($token); return [ 'username' => $user->username, 'expires_at' => $this->expiresAt($user), ]; } /** * 设新密码并作废 token(一次性)。 */ public function reset(string $token, string $password): void { $user = $this->userByToken($token); $user->password = $this->hasher->hash($password); $user->reset_password_token = null; $user->reset_password_sent_at = null; $user->save(); } private function userByToken(string $token): UserInfo { $user = UserInfo::where('reset_password_token', $this->digest($token)) ->whereNotNull('reset_password_sent_at') ->first(); // 不存在与已过期同样回 404:不给调用方区分两者的信号 if (! $user || $this->expiresAt($user)->isPast()) { abort(404, __('passwords.token')); } return $user; } private function expiresAt(UserInfo $user): CarbonInterface { // UserInfo 是 v2/v3 共享模型,没给这列配 cast,这里自己解析,不去改共享模型 return Date::parse($user->reset_password_sent_at)->addMinutes(self::TTL_MINUTES); } private function digest(string $token): string { return hash('sha256', $token); } }