feat: nhắc gia hạn QR theo dịch vụ, failover server hosting, CCCD admin, log cấp phát dịch vụ
- Mail campaign: mẫu nhắc gia hạn thanh toán QR, chọn dịch vụ + chu kỳ từng dòng (renewal_services) - Hosting: nhiều server Plesk cho một gói (service_plan_servers), chuyển server khi server chết - Admin xem/sửa giấy tờ định danh khách, tuỳ chọn gia hạn cho khách, nhật ký cấp phát dịch vụ - Đánh dấu mẫu email dịch vụ là non-marketing Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
047c23beff
commit
55a834a2f8
@@ -10,6 +10,7 @@ use App\Models\CustomerTrustedDevice;
|
||||
use App\Support\AutoEmailGate;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -19,7 +20,9 @@ class ClientTwoFactorService
|
||||
{
|
||||
public const CODE_TTL_MINUTES = 5;
|
||||
|
||||
public const RESEND_COOLDOWN_SECONDS = 60;
|
||||
public const RESEND_COOLDOWN_SECONDS = self::CODE_TTL_MINUTES * 60;
|
||||
|
||||
private const ENTRY_TTL_MINUTES = 15;
|
||||
|
||||
public const TRUSTED_DEVICE_DAYS = 90;
|
||||
|
||||
@@ -37,17 +40,50 @@ class ClientTwoFactorService
|
||||
/**
|
||||
* @return array{challenge: string, email: string, expires_in: int, resend_in: int}
|
||||
*/
|
||||
public function startLoginChallenge(Customer $customer): array
|
||||
public function startLoginChallenge(Customer $customer, ?string $ip, ?string $userAgent): array
|
||||
{
|
||||
$challenge = Str::random(64);
|
||||
$this->issueCode($this->challengeKey($challenge), $customer, self::PURPOSE_LOGIN);
|
||||
$pointerKey = $this->clientPointerKey($customer, $ip, $userAgent);
|
||||
|
||||
return [
|
||||
'challenge' => $challenge,
|
||||
'email' => $this->maskEmail($customer->email),
|
||||
'expires_in' => self::CODE_TTL_MINUTES * 60,
|
||||
'resend_in' => self::RESEND_COOLDOWN_SECONDS,
|
||||
];
|
||||
return Cache::lock($pointerKey . ':lock', 10)->block(5, function () use ($customer, $pointerKey) {
|
||||
$challenge = $this->activeChallenge($pointerKey);
|
||||
|
||||
if (!$challenge) {
|
||||
$challenge = Str::random(64);
|
||||
$this->issueCode($this->challengeKey($challenge), $customer, self::PURPOSE_LOGIN);
|
||||
Cache::put($pointerKey, Crypt::encryptString($challenge), now()->addMinutes(self::ENTRY_TTL_MINUTES));
|
||||
}
|
||||
|
||||
$remaining = max(Cache::get($this->challengeKey($challenge))['expires_at'] - now()->getTimestamp(), 0);
|
||||
|
||||
return [
|
||||
'challenge' => $challenge,
|
||||
'email' => $this->maskEmail($customer->email),
|
||||
'expires_in' => $remaining,
|
||||
'resend_in' => $remaining,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function activeChallenge(string $pointerKey): ?string
|
||||
{
|
||||
$stored = Cache::get($pointerKey);
|
||||
$challenge = $stored ? Crypt::decryptString($stored) : null;
|
||||
$entry = $challenge ? Cache::get($this->challengeKey($challenge)) : null;
|
||||
|
||||
if ($entry && $entry['expires_at'] > now()->getTimestamp()) {
|
||||
return $challenge;
|
||||
}
|
||||
|
||||
if ($challenge) {
|
||||
Cache::forget($this->challengeKey($challenge));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function clientPointerKey(Customer $customer, ?string $ip, ?string $userAgent): string
|
||||
{
|
||||
return 'client-2fa:client:' . hash('sha256', $customer->id . '|' . $ip . '|' . $userAgent);
|
||||
}
|
||||
|
||||
public function resendLoginChallenge(string $challenge): void
|
||||
@@ -186,7 +222,7 @@ class ClientTwoFactorService
|
||||
'attempts' => 0,
|
||||
'sent_at' => now()->getTimestamp(),
|
||||
'expires_at' => now()->addMinutes(self::CODE_TTL_MINUTES)->getTimestamp(),
|
||||
], now()->addMinutes(self::CODE_TTL_MINUTES));
|
||||
], now()->addMinutes(self::ENTRY_TTL_MINUTES));
|
||||
|
||||
AutoEmailGate::markManual();
|
||||
Mail::to($customer->email)->queue(new TwoFactorCode([
|
||||
@@ -199,6 +235,12 @@ class ClientTwoFactorService
|
||||
|
||||
private function checkCode(string $key, array $entry, string $code): void
|
||||
{
|
||||
if ($entry['expires_at'] <= now()->getTimestamp()) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => ['Mã xác thực đã hết hạn, vui lòng gửi lại mã.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (hash_equals($entry['code_hash'], hash('sha256', $key . $code))) {
|
||||
Cache::forget($key);
|
||||
|
||||
@@ -214,7 +256,7 @@ class ClientTwoFactorService
|
||||
]);
|
||||
}
|
||||
|
||||
Cache::put($key, $entry, max($entry['expires_at'] - now()->getTimestamp(), 1));
|
||||
Cache::put($key, $entry, now()->addMinutes(self::ENTRY_TTL_MINUTES));
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'code' => ['Mã xác thực không chính xác.'],
|
||||
|
||||
Reference in New Issue
Block a user