104 lines
3.2 KiB
PHP
104 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\OtpRequest;
|
|
use App\Models\Registration;
|
|
use App\Services\OtpSender\LogOtpSender;
|
|
use App\Services\OtpSender\OtpSenderInterface;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
|
|
class OtpService
|
|
{
|
|
public function sender(): OtpSenderInterface
|
|
{
|
|
$driver = config('pxg.wa_otp_driver', 'log');
|
|
// Add more drivers later (qontak, interakt, twilio, etc)
|
|
return match ($driver) {
|
|
'log' => new LogOtpSender(),
|
|
default => new LogOtpSender(),
|
|
};
|
|
}
|
|
|
|
public function sendPairingOtp(Registration $registration): void
|
|
{
|
|
$ttl = config('pxg.otp_ttl_minutes', 5);
|
|
$maxPer10m = config('pxg.otp_rate_limit_per_phone', 3);
|
|
|
|
$phone = $this->toE164($registration->player->phone);
|
|
|
|
// Basic rate-limit: count last 10 minutes
|
|
$recentCount = OtpRequest::where('phone_e164', $phone)
|
|
->where('created_at', '>=', now()->subMinutes(10))
|
|
->count();
|
|
|
|
if ($recentCount >= $maxPer10m) {
|
|
abort(429, 'Too many OTP requests. Please try again later.');
|
|
}
|
|
|
|
$otp = (string) random_int(100000, 999999);
|
|
|
|
OtpRequest::create([
|
|
'purpose' => 'PAIRING_VIEW',
|
|
'registration_id' => $registration->id,
|
|
'phone_e164' => $phone,
|
|
'otp_hash' => Hash::make($otp),
|
|
'expires_at' => now()->addMinutes($ttl),
|
|
'attempts' => 0,
|
|
'status' => 'SENT',
|
|
'request_ip' => request()->ip(),
|
|
'user_agent' => (string) request()->userAgent(),
|
|
]);
|
|
|
|
$msg = "TOURNAMENT PXG 2026\nYour OTP: {$otp}\nValid for {$ttl} minutes.";
|
|
$this->sender()->sendWhatsApp($phone, $msg);
|
|
}
|
|
|
|
public function verifyPairingOtp(Registration $registration, string $otp): void
|
|
{
|
|
$maxAttempts = config('pxg.otp_max_attempts', 5);
|
|
$phone = $this->toE164($registration->player->phone);
|
|
|
|
$req = OtpRequest::where('registration_id', $registration->id)
|
|
->where('phone_e164', $phone)
|
|
->whereIn('status', ['SENT'])
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if (!$req) abort(404, 'OTP not found. Please request OTP again.');
|
|
if ($req->expires_at->isPast()) {
|
|
$req->status = 'EXPIRED'; $req->save();
|
|
abort(410, 'OTP expired. Please request OTP again.');
|
|
}
|
|
if ($req->attempts >= $maxAttempts) {
|
|
$req->status = 'LOCKED'; $req->save();
|
|
abort(423, 'Too many attempts. Please request OTP again later.');
|
|
}
|
|
|
|
$req->attempts += 1;
|
|
$req->save();
|
|
|
|
if (!Hash::check($otp, $req->otp_hash)) {
|
|
abort(401, 'Invalid OTP.');
|
|
}
|
|
|
|
$req->status = 'VERIFIED';
|
|
$req->save();
|
|
}
|
|
|
|
private function toE164(string $phone): string
|
|
{
|
|
// Basic normalization for Indonesia numbers: 08xx -> +628xx
|
|
$p = preg_replace('/\D+/', '', $phone);
|
|
if (Str::startsWith($p, '0')) {
|
|
$p = '62'.substr($p, 1);
|
|
}
|
|
if (!Str::startsWith($p, '62')) {
|
|
// fallback
|
|
$p = '62'.$p;
|
|
}
|
|
return '+'.$p;
|
|
}
|
|
}
|