clientRepository = $clientRepository; } /** * Xác thực khách hàng và tạo access + refresh token */ public function authenticateClient(array $credentials): array { if (!$token = Auth::guard('client')->attempt($credentials)) { throw ValidationException::withMessages([ 'email' => ['Thông tin đăng nhập không chính xác.'], ]); } $client = Auth::guard('client')->user(); if ((int)$client->status !== 1) { $statusLabel = match ((int)$client->status) { 0 => 'PENDING_VERIFY', 2 => 'tạm ngưng hoạt động', 3 => 'bị khoá', default => 'vô hiệu hoá', }; if ($statusLabel === 'PENDING_VERIFY') { throw ValidationException::withMessages([ 'email' => ['PENDING_VERIFY'], ]); } throw ValidationException::withMessages([ 'email' => ["Tài khoản của bạn đã {$statusLabel}, vui lòng liên hệ hotline."], ]); } $refreshToken = $this->createRefreshToken($client->id); return [ 'client' => $client, 'token' => $token, 'refresh_token' => $refreshToken, ]; } /** * Đăng ký khách hàng mới — status=0 (chờ xác thực OTP), chưa cấp token. */ public function registerClient(array $data): array { $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); $client = $this->clientRepository->create([ 'fullname' => $data['fullname'], 'phone' => $data['phone'], 'email' => $data['email'], 'password' => bcrypt($data['password']), 'status' => 0, 'email_verify_code' => $code, 'email_verify_code_expired_at' => now()->addMinutes(5), ]); $this->sendVerifyEmail($client, $code); return ['client' => $client]; } /** * Xác thực OTP — kích hoạt tài khoản, trả về token. */ public function verifyEmail(string $email, string $code): array { $client = \App\Models\Customer::whereRaw('LOWER(email) = ?', [strtolower(trim($email))])->first(); if (! $client || $client->email_verify_code !== $code) { throw ValidationException::withMessages([ 'code' => ['Mã xác thực không chính xác.'], ]); } if (! $client->email_verify_code_expired_at || now()->gt($client->email_verify_code_expired_at)) { throw ValidationException::withMessages([ 'code' => ['Mã xác thực đã hết hạn, vui lòng yêu cầu gửi lại.'], ]); } $client->status = 1; $client->email_verify_code = null; $client->email_verify_code_expired_at = null; $client->email_verified_at = now(); $client->save(); $token = JWTAuth::fromUser($client); $refreshToken = $this->createRefreshToken($client->id); $this->sendWelcomeEmail($client); return [ 'client' => $client, 'token' => $token, 'refresh_token' => $refreshToken, ]; } /** * Gửi lại OTP xác thực (rate limit bởi caller nếu cần). */ public function resendVerifyCode(string $email): void { $client = \App\Models\Customer::whereRaw('LOWER(email) = ?', [strtolower(trim($email))])->first(); if (! $client || $client->status !== 0) { return; } // Nếu mã cũ còn hạn thì không gửi lại — phải đợi hết hạn if ($client->email_verify_code_expired_at && now()->lt($client->email_verify_code_expired_at)) { throw ValidationException::withMessages([ 'email' => ['Mã xác thực vẫn còn hiệu lực, vui lòng kiểm tra email của bạn.'], ]); } $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); $client->email_verify_code = $code; $client->email_verify_code_expired_at = now()->addMinutes(5); $client->save(); $this->sendVerifyEmail($client, $code); } protected function sendVerifyEmail($client, string $code): void { try { Mail::to($client->email)->later(now()->addSeconds(5), new VerifyAccountMail([ 'fullname' => $client->fullname, 'email' => $client->email, 'verify_code' => $code, ])); } catch (\Throwable $e) { Log::warning('Send verify email failed: ' . $e->getMessage(), [ 'customer_id' => $client->id, 'email' => $client->email, ]); } } /** * Gửi email chào mừng sau khi xác thực tài khoản thành công. */ protected function sendWelcomeEmail($client): void { try { Mail::to($client->email)->send(new WelcomeMail([ 'fullname' => $client->fullname, 'email' => $client->email, ])); } catch (\Throwable $e) { Log::warning('Send welcome email failed: ' . $e->getMessage(), [ 'customer_id' => $client->id, 'email' => $client->email, ]); } } /** * Tạo refresh token mới, lưu hash vào DB, trả về raw token. */ public function createRefreshToken(int $customerId): string { $raw = Str::random(64); $hash = hash('sha256', $raw); RefreshToken::create([ 'customer_id' => $customerId, 'token' => $hash, 'expires_at' => now()->addDays(self::REFRESH_TOKEN_TTL_DAYS), ]); return $raw; } /** * Xác minh refresh token, xoay vòng (rotation), trả về access token mới + refresh token mới. * Ném ValidationException nếu token không hợp lệ hoặc đã hết hạn. */ public function rotateRefreshToken(string $rawToken): array { $hash = hash('sha256', $rawToken); $record = RefreshToken::where('token', $hash)->first(); if (!$record || $record->isExpired()) { // Xoá token cũ nếu tồn tại nhưng hết hạn $record?->delete(); throw ValidationException::withMessages([ 'refresh_token' => ['Refresh token không hợp lệ hoặc đã hết hạn.'], ]); } $customer = $record->customer; if (!$customer || (int)$customer->status !== 1) { $record->delete(); throw ValidationException::withMessages([ 'refresh_token' => ['Tài khoản không hợp lệ.'], ]); } // Rotation: xoá token cũ $record->delete(); // Tạo access token mới $newAccessToken = JWTAuth::fromUser($customer); // Tạo refresh token mới $newRefreshToken = $this->createRefreshToken($customer->id); return [ 'client' => $customer, 'token' => $newAccessToken, 'refresh_token' => $newRefreshToken, ]; } /** * Huỷ refresh token khi đăng xuất. */ public function revokeRefreshToken(string $rawToken): void { $hash = hash('sha256', $rawToken); RefreshToken::where('token', $hash)->delete(); } }