PXG_2026_API/app/Services/OtpService.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;
}
}