This commit is contained in:
baodev0
2026-09-23 15:17:18 +07:00
parent cd38c5d6d0
commit 82a71244ca
107 changed files with 3332 additions and 3306 deletions
+67 -17
View File
@@ -4,7 +4,7 @@ namespace App\Services\Api\Client;
use App\Http\Services\Telegram;
use App\Mail\Admin\TrialRegistrationRequest as TrialRegistrationRequestMail;
use App\Mail\Customer\PasswordResetByAdmin;
use App\Mail\Customer\PasswordResetLink;
use App\Mail\Customer\VerifyAccount as VerifyAccountMail;
use App\Mail\Customer\Welcome as WelcomeMail;
use App\Models\Customer;
@@ -12,10 +12,11 @@ use App\Models\RefreshToken;
use App\Models\Setting;
use App\Repositories\Api\Client\ClientRepositoryInterface;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Tymon\JWTAuth\Facades\JWTAuth;
@@ -25,6 +26,8 @@ class ClientAuthService
// Refresh token lifetime in days
const REFRESH_TOKEN_TTL_DAYS = 30;
const VERIFY_MAX_ATTEMPTS = 5;
protected $clientRepository;
public function __construct(ClientRepositoryInterface $clientRepository)
@@ -143,12 +146,28 @@ class ClientAuthService
{
$client = \App\Models\Customer::whereRaw('LOWER(email) = ?', [strtolower(trim($email))])->first();
if (! $client || $client->email_verify_code !== $code) {
// Chống brute-force mã 6 số: sai quá VERIFY_MAX_ATTEMPTS lần thì huỷ mã, buộc gửi lại.
$attemptKey = 'verify-email:' . strtolower(trim($email));
if (! $client || ! $client->email_verify_code || ! hash_equals($client->email_verify_code, $code)) {
if ($client?->email_verify_code && RateLimiter::hit($attemptKey, 600) >= self::VERIFY_MAX_ATTEMPTS) {
RateLimiter::clear($attemptKey);
$client->email_verify_code = null;
$client->email_verify_code_expired_at = null;
$client->save();
throw ValidationException::withMessages([
'code' => ['Nhập sai quá nhiều lần, vui lòng yêu cầu gửi lại mã xác thực.'],
]);
}
throw ValidationException::withMessages([
'code' => ['Mã xác thực không chính xác.'],
]);
}
RateLimiter::clear($attemptKey);
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.'],
@@ -305,10 +324,11 @@ class ClientAuthService
}
/**
* Cấp mật khẩu mới ngẫu nhiên khi khách hàng quên mật khẩu. Không tiết lộ
* email có tồn tại hay không — trả về null nếu không tìm thấy tài khoản.
* Gửi link đặt lại mật khẩu (token một lần, hết hạn theo auth.passwords.users.expire).
* Không đổi mật khẩu ngay — chỉ chủ hộp thư mới đổi được. Trả về null nếu không có
* tài khoản; caller không được tiết lộ kết quả cho client.
*/
public function resetForgottenPassword(string $email): ?Customer
public function sendPasswordResetLink(string $email): ?Customer
{
$customer = Customer::whereRaw('LOWER(email) = ?', [strtolower(trim($email))])->first();
@@ -316,22 +336,49 @@ class ClientAuthService
return null;
}
$newPassword = Str::password(16);
// Broker tự throttle theo email (auth.passwords.users.throttle).
Password::broker('users')->sendResetLink(
['email' => $customer->email],
function (Customer $customer, string $token) {
$url = rtrim(config('app.frontend_url'), '/') . '/dat-lai-mat-khau?' . http_build_query([
'token' => $token,
'email' => $customer->email,
]);
DB::transaction(function () use ($customer, $newPassword) {
$customer->password = Hash::make($newPassword);
$customer->save();
Mail::to($customer->email)->later(now()->addSeconds(5), new PasswordResetByAdmin([
'fullname' => $customer->fullname,
'email' => $customer->email,
'password' => $newPassword,
]));
});
Mail::to($customer->email)->queue(new PasswordResetLink([
'fullname' => $customer->fullname,
'reset_url' => $url,
]));
},
);
return $customer;
}
/**
* Đặt lại mật khẩu bằng token trong email. Thành công thì thu hồi toàn bộ refresh token
* để các phiên cũ (có thể của kẻ gian) phải đăng nhập lại.
*/
public function resetPassword(array $data): ?Customer
{
$resetCustomer = null;
$status = Password::broker('users')->reset($data, function (Customer $customer, string $password) use (&$resetCustomer) {
$customer->password = Hash::make($password);
$customer->save();
RefreshToken::where('customer_id', $customer->id)->delete();
$resetCustomer = $customer;
});
if ($status !== Password::PASSWORD_RESET) {
throw ValidationException::withMessages([
'token' => ['Link đặt lại mật khẩu không hợp lệ hoặc đã hết hạn.'],
]);
}
return $resetCustomer;
}
/**
* Cập nhật thông tin cá nhân. Trả về old/dirty values để caller ghi activity log.
*/
@@ -375,5 +422,8 @@ class ClientAuthService
$client->password = Hash::make($newPassword);
$client->save();
// Access token cũ tự vô hiệu nhờ claim `pv`; thu hồi luôn refresh token để mọi phiên phải đăng nhập lại.
RefreshToken::where('customer_id', $client->id)->delete();
}
}