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>
57 lines
1.8 KiB
PHP
57 lines
1.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1\Auth;
|
|
|
|
use App\Http\Controllers\Api\V1\ApiController;
|
|
use App\Http\Requests\Auth\ResetPasswordRequest;
|
|
use App\Models\OtpCode;
|
|
use App\Models\User;
|
|
use App\Services\Otp\OtpService;
|
|
use App\Services\Otp\OtpVerifyResult;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Laravel\Sanctum\PersonalAccessToken;
|
|
|
|
class ResetPasswordController extends ApiController
|
|
{
|
|
public function __invoke(ResetPasswordRequest $request, OtpService $otp): JsonResponse
|
|
{
|
|
$data = $request->validated();
|
|
$user = User::where('phone', $data['phone'])->first();
|
|
|
|
if (! $user) {
|
|
return $this->fail('Invalid code', ['code' => ['not_found']], 422);
|
|
}
|
|
|
|
$result = $otp->verify($data['phone'], OtpCode::PURPOSE_PASSWORD_RESET, $data['code']);
|
|
|
|
if (! $result->isOk()) {
|
|
return $this->fail(
|
|
$this->messageFor($result->status),
|
|
['code' => [$result->status]],
|
|
422,
|
|
);
|
|
}
|
|
|
|
$user->forceFill(['password' => Hash::make($data['password'])])->save();
|
|
|
|
PersonalAccessToken::query()
|
|
->where('tokenable_type', $user->getMorphClass())
|
|
->where('tokenable_id', $user->id)
|
|
->delete();
|
|
|
|
return $this->ok(null, 'Password reset successful. Please log in.');
|
|
}
|
|
|
|
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 => 'Reset failed',
|
|
};
|
|
}
|
|
}
|