Closes Module 1: 9 auth endpoints under /api/v1/auth, OTP via SMS (Semaphore + log + fake drivers), role middleware, role + admin seeders, 27 feature tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.2 KiB
PHP
67 lines
2.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Auth;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Http\Requests\Auth\VerifyOtpRequest;
|
|
use App\Http\Resources\UserResource;
|
|
use App\Models\OtpCode;
|
|
use App\Models\User;
|
|
use App\Services\Otp\OtpService;
|
|
use App\Services\Otp\OtpVerifyResult;
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
class VerifyOtpController extends ApiController
|
|
{
|
|
public function __invoke(VerifyOtpRequest $request, OtpService $otp): JsonResponse
|
|
{
|
|
$data = $request->validated();
|
|
|
|
$result = $otp->verify($data['phone'], $data['purpose'], $data['code']);
|
|
|
|
if (! $result->isOk()) {
|
|
return $this->fail(
|
|
$this->messageFor($result->status),
|
|
['code' => [$result->status]],
|
|
422,
|
|
);
|
|
}
|
|
|
|
$user = User::where('phone', $data['phone'])->first();
|
|
|
|
if ($user && in_array($data['purpose'], [OtpCode::PURPOSE_REGISTER, OtpCode::PURPOSE_PHONE_VERIFY], true)) {
|
|
$user->forceFill([
|
|
'phone_verified_at' => now(),
|
|
'status' => $user->status === User::STATUS_PENDING ? User::STATUS_ACTIVE : $user->status,
|
|
])->save();
|
|
}
|
|
|
|
$payload = [
|
|
'verified' => true,
|
|
'purpose' => $data['purpose'],
|
|
];
|
|
|
|
if ($data['purpose'] === OtpCode::PURPOSE_LOGIN && $user) {
|
|
$token = $user->createToken($request->userAgent() ?? 'otp-login');
|
|
$payload['user'] = new UserResource($user);
|
|
$payload['token'] = $token->plainTextToken;
|
|
$payload['token_type'] = 'Bearer';
|
|
} elseif ($user) {
|
|
$payload['user'] = new UserResource($user);
|
|
}
|
|
|
|
return $this->ok($payload, 'OTP verified');
|
|
}
|
|
|
|
private function messageFor(string $status): string
|
|
{
|
|
return match ($status) {
|
|
OtpVerifyResult::STATUS_INVALID => 'Invalid code',
|
|
OtpVerifyResult::STATUS_EXPIRED => 'Code expired',
|
|
OtpVerifyResult::STATUS_EXHAUSTED => 'Too many attempts. Request a new code.',
|
|
OtpVerifyResult::STATUS_NOT_FOUND => 'No active code for this destination',
|
|
default => 'Verification failed',
|
|
};
|
|
}
|
|
}
|