Skip to content

Commit

Permalink
feat: update the code generation and error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
ibnnajjaar committed Sep 10, 2024
1 parent 061d166 commit 177b1dd
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 27 deletions.
2 changes: 1 addition & 1 deletion lang/en/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
'verification_code' => [
'invalid' => 'The token is invalid.',
'expired' => 'The token has expired.',
'locked' => 'The number is locked due to too many attempts.'
'locked' => 'The number is locked due to too many attempts. You can try again in :time.'
],
'messages' => [
'verification_code_verified' => "Your mobile number has been successfully verified.",
Expand Down
5 changes: 5 additions & 0 deletions src/MobileVerificationServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use League\OAuth2\Server\AuthorizationServer;
use Laravel\Passport\Bridge\RefreshTokenRepository;
use Javaabu\MobileVerification\GrantType\MobileGrant;
use Javaabu\MobileVerification\Support\VerificationCodeGenerator;
use Javaabu\MobileVerification\Middlewares\AllowMobileVerifiedUsersOnly;

class MobileVerificationServiceProvider extends ServiceProvider
Expand Down Expand Up @@ -63,6 +64,10 @@ public function register()
});

app('router')->aliasMiddleware('mobile-verified', AllowMobileVerifiedUsersOnly::class);

$this->app->bind(VerificationCodeGenerator::class, function () {
return new VerificationCodeGenerator();
});
}

protected function makeGrant(): MobileGrant
Expand Down
14 changes: 5 additions & 9 deletions src/Models/MobileNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Javaabu\MobileVerification\Contracts\HasMobileNumber;
use Javaabu\MobileVerification\Factories\MobileNumberFactory;
use Javaabu\MobileVerification\Support\VerificationCodeUpdater;
use Javaabu\MobileVerification\Support\VerificationCodeGenerator;
use Javaabu\MobileVerification\Contracts\MobileNumber as MobileNumberContract;

class MobileNumber extends Model implements MobileNumberContract
Expand Down Expand Up @@ -216,15 +218,9 @@ public function clearVerificationCode(): void
*/
public function generateVerificationCode(): string
{
$verification_code = $this->randomVerificationCode();

$this->attempts = 0;
$this->verification_code = $verification_code;
$this->verification_code_created_at = Carbon::now();
$this->verification_code_id = Str::uuid();
$this->save();

return $verification_code;
/* @var VerificationCodeUpdater $verificationCodeUpdater */
$verificationCodeUpdater = app(VerificationCodeUpdater::class);
return $verificationCodeUpdater->handle($this);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/IsValidMobileNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ public function validate(string $attribute, mixed $value, Closure $fail): void

if ($mobile_number && $this->can_send_otp) {
if (! $mobile_number->can_request_code) {
$attempts_expiry_seconds = seconds_to_human_readable($mobile_number->attempts_expiry_seconds);
$fail(trans('mobile-verification::strings.validation.number.locked', ['time' => $attempts_expiry_seconds]));
$time = seconds_to_human_readable(now()->diffInSeconds($mobile_number->attempts_expiry_at));
$fail(trans('mobile-verification::strings.validation.number.locked', ['time' => $time]));
}

if ($mobile_number->was_sent_recently) {
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/IsValidVerificationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
}

if ($mobile_number->is_locked) {
$fail(trans('mobile-verification::strings.validation.verification_code.locked'));
$time = seconds_to_human_readable(now()->diffInSeconds($mobile_number->attempts_expiry_at));
$fail(trans('mobile-verification::strings.validation.verification_code.locked', ['time' => $time]));
}

if ($mobile_number->is_verification_code_expired) {
Expand Down
15 changes: 15 additions & 0 deletions src/Support/VerificationCodeGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Javaabu\MobileVerification\Support;

use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class VerificationCodeGenerator
{
public function handle(): string
{
return str_pad(rand(0, 999999), 6, 0, STR_PAD_LEFT);
}
}
28 changes: 28 additions & 0 deletions src/Support/VerificationCodeUpdater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Javaabu\MobileVerification\Support;

use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class VerificationCodeUpdater
{
public function __construct(
public VerificationCodeGenerator $verificationCodeGenerator
)
{
}
public function handle(Model $mobile_number): string
{
$verification_code = $this->verificationCodeGenerator->handle();

$mobile_number->attempts = 0;
$mobile_number->verification_code = $verification_code;
$mobile_number->verification_code_created_at = Carbon::now();
$mobile_number->verification_code_id = Str::uuid();
$mobile_number->save();

return $verification_code;
}
}
1 change: 1 addition & 0 deletions src/Traits/SendsVerificationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function getVerificationCodeRequestValidator(Request $request): Validator

public function getVerificationCodeRequestValidationRules(Request $request): array
{

$valid_mobile_number_rule = (new IsValidMobileNumber(
$this->getUserType($request),
$this->getCountryCodeInputKey()
Expand Down
78 changes: 65 additions & 13 deletions tests/Feature/Controllers/Api/ApiTokenLoginControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Javaabu\MobileVerification\Tests\Feature\Controllers\Web;

use Laravel\Passport\Client;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Laravel\Passport\ClientRepository;
Expand All @@ -20,9 +21,13 @@ class ApiTokenLoginControllerTest extends TestCase
// an unauthorized user cannot visit auth protected routes
public function an_unauthorized_user_cannot_visit_auth_protected_routes()
{
Config::set('auth.guards.api.provider', 'users');
Config::set('auth.guards.api.driver', 'passport');
Config::set('auth.providers.users.model', User::class);

$this->get('/api/protected')
->assertStatus(302)
->assertRedirect(route('login'));
->assertStatus(302)
->assertRedirect(route('login'));
}


Expand All @@ -34,19 +39,19 @@ public function can_obtain_an_access_token_using_a_valid_verification_code()
MobileNumber::unguard();

$mobile_number = MobileNumber::create([
'number' => '7528222',
'number' => '7528222',
'country_code' => '960',
'user_type' => 'user',
'user_id' => $user->id,
'user_type' => 'user',
'user_id' => $user->id,
]);

$verification_code = $mobile_number->generateVerificationCode();

$this->assertDatabaseHas('mobile_numbers', [
'number' => '7528222',
'number' => '7528222',
'country_code' => '960',
'user_id' => $user->id,
'user_type' => 'user',
'user_id' => $user->id,
'user_type' => 'user',
]);

$grantClient = $this->app
Expand All @@ -57,16 +62,63 @@ public function can_obtain_an_access_token_using_a_valid_verification_code()
Config::set('auth.providers.users.model', User::class);

$response = $this->postJson('/oauth/token', [
'grant_type' => 'mobile',
'client_id' => $grantClient->id,
'client_secret' => $grantClient->secret,
'number' => '7528222',
'country_code' => '960',
'grant_type' => 'mobile',
'client_id' => $grantClient->id,
'client_secret' => $grantClient->secret,
'number' => '7528222',
'country_code' => '960',
'verification_code' => $verification_code,
]);

$response->assertStatus(200);
$response->assertJsonStructure(['token_type', 'access_token', 'refresh_token', 'expires_in']);
}

public function test_authenticated_user_can_visit_protected_routes()
{
$user = User::factory()->create();
$access_token = $this->getAccessToken($user);

$response = $this->json('GET', '/api/protected', [], [
'Authorization' => 'Bearer ' . $access_token,
]);

$response->assertStatus(200);
$response->assertSeeText('Protected route');
}

public function getAccessToken(?User $user = null): string
{
$user ??= User::factory()->create();
MobileNumber::unguard();

$mobile_number = MobileNumber::create([
'number' => '7528222',
'country_code' => '960',
'user_type' => 'user',
'user_id' => $user->id,
]);

$verification_code = $mobile_number->generateVerificationCode();

$grantClient = $this->app->make(ClientRepository::class)
->createPasswordGrantClient(null, 'Test', 'http://localhost');

Config::set('auth.guards.api.provider', 'users');
Config::set('auth.guards.api.driver', 'passport');
Config::set('auth.providers.users.model', User::class);

$response = $this->postJson('/oauth/token', [
'grant_type' => 'mobile',
'client_id' => $grantClient->id,
'client_secret' => $grantClient->secret,
'number' => '7528222',
'country_code' => '960',
'verification_code' => $verification_code,
]);

$content = json_decode($response->content(), true);
return $content['access_token'];
}

}
112 changes: 112 additions & 0 deletions tests/Feature/VerificationCodeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Javaabu\MobileVerification\Tests\Feature;

use Javaabu\MobileVerification\Tests\TestCase;
use Javaabu\MobileVerification\Models\MobileNumber;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Javaabu\MobileVerification\Tests\TestSupport\Models\User;
use Javaabu\MobileVerification\Support\VerificationCodeGenerator;

class VerificationCodeTest extends TestCase
{
use LazilyRefreshDatabase;

public function test_user_can_request_verification_code()
{
$this->withoutExceptionHandling();
$user = $this->getUserWithMobileNumber();


$this->assertDatabaseHas('mobile_numbers', [
'number' => '7528222',
'country_code' => '960',
'user_id' => $user->id,
'user_type' => 'user',
'verification_code' => null,
]);

$this->postJson('/api/mobile-verifications/verification-code', [
'user_type' => 'user',
'number' => '7528222',
]);

$mobile_number = $user->phone()->first();
$this->assertNotNull($mobile_number->verification_code);
}

public function test_too_many_invalid_attempts_will_lock_the_mobile_number()
{
$user = User::factory()->create();
MobileNumber::unguard();

$mobile_number = MobileNumber::create([
'number' => '7528222',
'country_code' => '960',
'user_type' => 'user',
'user_id' => $user->id,
]);

$this->mock(VerificationCodeGenerator::class, function ($mock) use ($mobile_number) {
$mock->shouldReceive('handle')
->andReturn('123456');
});

$response = $this->postJson('/api/mobile-verifications/verification-code', [
'user_type' => 'user',
'number' => '7528222',
]);

$verification_code_id = $response->json()['verification_code_id'];
$max_attempts = config('mobile-verification.max_attempts');
$try = 1;

foreach (range(1, $max_attempts + 20) as $attempt) {
$this->postJson('/api/mobile-verifications/verify', [
'user_type' => 'user',
'number' => '7528222',
'verification_code' => '999999',
'verification_code_id' => $verification_code_id,
]);

$mobile_number->refresh();
$this->assertEquals($mobile_number->attempts, $try);

if ($max_attempts == $try) {
$mobile_number->refresh();
$this->assertEquals($mobile_number->is_locked, true);
}

$try++;
}

$this->postJson('/api/mobile-verifications/verify', [
'user_type' => 'user',
'number' => '7528222',
'verification_code' => '999999',
'verification_code_id' => $verification_code_id,
])
->assertJsonValidationErrors(['verification_code']);

$this->postJson('/api/mobile-verifications/verification-code', [
'user_type' => 'user',
'number' => '7528222',
])
->assertJsonValidationErrors(['number']);
}

public function getUserWithMobileNumber(string $number = '7528222'): User
{
$user = User::factory()->create();
MobileNumber::unguard();

$mobile_number = MobileNumber::create([
'number' => $number,
'country_code' => '960',
'user_type' => 'user',
'user_id' => $user->id,
]);

return $user;
}
}
Loading

0 comments on commit 177b1dd

Please sign in to comment.