Skip to content

Commit a1f3927

Browse files
feat: additional changes
1 parent 0498ff3 commit a1f3927

7 files changed

Lines changed: 54 additions & 29 deletions

File tree

database/migrations/create_lockout_logs_table.php.stub

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ return new class extends Migration
1010
{
1111
Schema::create('lockout_logs', function (Blueprint $table) {
1212
$table->id();
13-
$table->nullableMorphs('model');
1413
$table->string('identifier')->nullable();
1514
$table->string('ip_address')->nullable();
1615
$table->string('user_agent')->nullable();

database/migrations/create_model_lockouts_table.php.stub

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ return new class extends Migration
2727
$table->id();
2828

2929
// Polymorphic relation
30-
$table->string('model_type');
31-
$table->unsignedBigInteger('model_id');
30+
$table->morphs('model');
3231

3332
// Lock tracking
3433
$table->timestamp('locked_at')->useCurrent();

src/Http/Controllers/UnlockController.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Beliven\Lockout\Http\Controllers;
44

55
use Beliven\Lockout\Facades\Lockout;
6-
use Beliven\Lockout\Models\ModelLockout;
76
use Illuminate\Http\RedirectResponse;
87
use Illuminate\Http\Request;
98

@@ -12,9 +11,7 @@ class UnlockController
1211
public function __invoke(Request $request): RedirectResponse
1312
{
1413
$identifier = $this->resolveIdentifier($request);
15-
$model = ModelLockout::active()
16-
->where('identifier', $identifier)
17-
->first()?->model;
14+
$model = Lockout::getLoginModel($identifier);
1815

1916
if (!$model) {
2017
return $this->redirectWithError();

src/Http/Middleware/EnsureUserIsNotLocked.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Beliven\Lockout\Http\Middleware;
44

55
use Beliven\Lockout\Facades\Lockout;
6-
use Beliven\Lockout\Models\ModelLockout;
76
use Closure;
87
use Illuminate\Database\Eloquent\Model;
98
use Illuminate\Http\JsonResponse;
@@ -21,9 +20,7 @@ public function handle($request, Closure $next)
2120

2221
// If a model exists for the identifier and it has an active persistent lock,
2322
// short-circuit with a 429 response.
24-
$model = ModelLockout::active()
25-
->where('identifier', $identifier)
26-
->first()?->model;
23+
$model = Lockout::getLoginModel($identifier);
2724

2825
if ($model && $this->modelHasActiveLock($model)) {
2926
return $this->lockedResponse();

src/Listeners/MarkModelAsLocked.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
namespace Beliven\Lockout\Listeners;
44

55
use Beliven\Lockout\Events\EntityLocked;
6-
use Beliven\Lockout\Lockout as LockoutService;
7-
use Beliven\Lockout\Models\ModelLockout;
6+
use Beliven\Lockout\Facades\Lockout;
87
use Throwable;
98

109
class MarkModelAsLocked
@@ -19,11 +18,12 @@ public function handle(EntityLocked $event): void
1918
try {
2019
$identifier = $event->identifier;
2120

22-
// Resolve the Lockout service and model for the identifier.
23-
$lockout = app(LockoutService::class);
24-
$model = ModelLockout::active()
25-
->where('identifier', $identifier)
26-
->first()?->model;
21+
// Resolve the concrete model via the Lockout facade (single-model strategy).
22+
try {
23+
$model = Lockout::getLoginModel($identifier);
24+
} catch (Throwable $_) {
25+
$model = null;
26+
}
2727

2828
if (!$model) {
2929
return;

src/Lockout.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ public function attemptLockout(string $id, object $data): bool
7878

7979
public function attemptSendLockoutNotification(string $id, object $data): void
8080
{
81-
// When the use disable the unlock via notification feature
81+
// When the user disables the unlock via notification feature
8282
// we can skip sending the notification
8383
if (!$this->unlockViaNotification) {
8484
return;
8585
}
8686

87-
// Check also if the column is a valid email format
87+
// Check also if the identifier is a valid email format
8888
if (!filter_var($id, FILTER_VALIDATE_EMAIL)) {
8989
return;
9090
}
@@ -99,10 +99,8 @@ public function attemptSendLockoutNotification(string $id, object $data): void
9999

100100
$notification = new $notificationClass($id, $this->decayMinutes, $signedUnlockUrl);
101101

102-
$model = ModelLockout::active()
103-
->where('identifier', $id)
104-
->first()?->model;
105-
102+
// Resolve the login model using the single-model strategy configured in auth.providers.users.model
103+
$model = $this->getLoginModel($id);
106104
if (!$model) {
107105
return;
108106
}
@@ -119,6 +117,20 @@ public function getLoginField(): string
119117
return config('lockout.login_field', 'email');
120118
}
121119

120+
public function getLoginModelClass(): string
121+
{
122+
$loginField = $this->getLoginField();
123+
124+
return config('auth.providers.users.model');
125+
}
126+
127+
public function getLoginModel(string $identifier): ?Model
128+
{
129+
$modelClass = $this->getLoginModelClass();
130+
131+
return $modelClass::where($this->getLoginField(), $identifier)->first();
132+
}
133+
122134
protected function throttleKey(string $id): string
123135
{
124136
return 'login-attempts:' . $id;
@@ -148,6 +160,18 @@ protected function createLog(string $id, object $data): void
148160
$logModel->user_agent = $data->user_agent ?? null;
149161
$logModel->attempted_at = now();
150162

163+
// Attempt to associate the created log with the configured login model
164+
// (single-model strategy). Be defensive: swallow any errors so tests and
165+
// constrained environments without the users table do not fail.
166+
try {
167+
$model = $this->getLoginModel($id);
168+
if ($model instanceof Model) {
169+
$logModel->model()->associate($model);
170+
}
171+
} catch (\Throwable $_) {
172+
// ignore association failures
173+
}
174+
151175
$logModel->save();
152176
}
153177
}

tests/Unit/Controllers/UnlockControllerUnitTest.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@ public function save(array $options = [])
3939
}
4040
};
4141

42-
// Expect the Lockout facade to return our model for the identifier
43-
Lockout::shouldReceive('getLoginModel')->once()->with($identifier)->andReturn($model);
42+
// Mock the Lockout service and bind it into the container so the controller
43+
// resolves the mocked instance instead of the facade. This mirrors how the
44+
// listener and other code resolve the service via the container.
45+
$mockService = Mockery::mock(\Beliven\Lockout\Lockout::class);
46+
$mockService->shouldReceive('getLoginModel')->once()->with($identifier)->andReturn($model);
47+
app()->instance(\Beliven\Lockout\Lockout::class, $mockService);
4448

4549
$request = Request::create('/lockout/unlock', 'GET', ['identifier' => $identifier]);
4650

@@ -81,7 +85,10 @@ public function save(array $options = [])
8185
// Set a locked_at initially to ensure legacy behavior would have cleared it.
8286
$model->locked_at = now();
8387

84-
Lockout::shouldReceive('getLoginModel')->once()->with($identifier)->andReturn($model);
88+
// Bind a mocked Lockout service into the container for this test case.
89+
$mockService = Mockery::mock(\Beliven\Lockout\Lockout::class);
90+
$mockService->shouldReceive('getLoginModel')->once()->with($identifier)->andReturn($model);
91+
app()->instance(\Beliven\Lockout\Lockout::class, $mockService);
8592

8693
$request = Request::create('/lockout/unlock', 'GET', ['identifier' => $identifier]);
8794

@@ -99,8 +106,10 @@ public function save(array $options = [])
99106
it('redirects to login with error when model is not found', function () {
100107
$identifier = 'nonexistent@example.test';
101108

102-
// Lockout returns null when no model found
103-
Lockout::shouldReceive('getLoginModel')->once()->with($identifier)->andReturn(null);
109+
// Bind a mocked Lockout service that returns null so controller handles not-found flow.
110+
$mockService = Mockery::mock(\Beliven\Lockout\Lockout::class);
111+
$mockService->shouldReceive('getLoginModel')->once()->with($identifier)->andReturn(null);
112+
app()->instance(\Beliven\Lockout\Lockout::class, $mockService);
104113

105114
$request = Request::create('/lockout/unlock', 'GET', ['identifier' => $identifier]);
106115

0 commit comments

Comments
 (0)