Skip to content

Commit f822726

Browse files
committed
feat: added login via magic-link
1 parent 2ab65e1 commit f822726

19 files changed

+665
-364
lines changed

_ide_helper.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
/**
66
* A helper file for Laravel, to provide autocomplete information to your IDE
7-
* Generated for Laravel 10.7.1.
7+
* Generated for Laravel 10.9.0.
88
*
99
* This file should not be included in your code, only analyzed by your IDE!
1010
*
@@ -9751,8 +9751,8 @@ public static function pool($callback)
97519751
/**
97529752
* Start defining a series of piped processes.
97539753
*
9754-
* @param callable $callback
9755-
* @return \Illuminate\Process\Pipe
9754+
* @param callable|array $callback
9755+
* @return \Illuminate\Contracts\Process\ProcessResult
97569756
* @static
97579757
*/
97589758
public static function pipe($callback, $output = null)
@@ -18204,7 +18204,7 @@ class Str {
1820418204
*
1820518205
*
1820618206
* @template TKey of array-key
18207-
* @template TValue
18207+
* @template-covariant TValue
1820818208
* @implements \ArrayAccess<TKey, TValue>
1820918209
* @implements \Illuminate\Support\Enumerable<TKey, TValue>
1821018210
*/
@@ -19247,7 +19247,7 @@ public static function registerErrorHandler()
1924719247
/**
1924819248
*
1924919249
*
19250-
* @param \Spatie\FlareClient\FlareMiddleware\FlareMiddleware|array<FlareMiddleware>|\Spatie\FlareClient\class-string<FlareMiddleware> $middleware
19250+
* @param \Spatie\FlareClient\FlareMiddleware\FlareMiddleware|array<FlareMiddleware>|\Spatie\FlareClient\class-string<FlareMiddleware>|callable $middleware
1925119251
* @return \Spatie\FlareClient\Flare
1925219252
* @static
1925319253
*/
@@ -21028,7 +21028,7 @@ public static function orWhereMorphRelation($relation, $types, $column, $operato
2102821028
* Add a morph-to relationship condition to the query.
2102921029
*
2103021030
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
21031-
* @param \Illuminate\Database\Eloquent\Model|string $model
21031+
* @param \Illuminate\Database\Eloquent\Model|string|null $model
2103221032
* @return \Illuminate\Database\Eloquent\Builder|static
2103321033
* @static
2103421034
*/
@@ -21056,7 +21056,7 @@ public static function whereNotMorphedTo($relation, $model, $boolean = 'and')
2105621056
* Add a morph-to relationship condition to the query with an "or where" clause.
2105721057
*
2105821058
* @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation
21059-
* @param \Illuminate\Database\Eloquent\Model|string $model
21059+
* @param \Illuminate\Database\Eloquent\Model|string|null $model
2106021060
* @return \Illuminate\Database\Eloquent\Builder|static
2106121061
* @static
2106221062
*/

_ide_helper_actions.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
<?php
22

3-
namespace App\Actions\Admin\CMS;
3+
namespace App\Actions\Admin\Cms;
44

55
/**
66
*/
7-
class ContactMessagesDelete
7+
class ContactMessages
88
{
99
}
10-
namespace App\Actions\Admin\Cms;
11-
1210
/**
1311
*/
14-
class ContactMessages
12+
class ContactMessagesDelete
1513
{
1614
}
1715
namespace App\Actions\Admin;
@@ -57,26 +55,19 @@ class Logout
5755
class Register
5856
{
5957
}
60-
namespace App\Actions\Auth\Pages;
61-
62-
/**
63-
*/
64-
class ForgotPassword
65-
{
66-
}
6758
/**
6859
*/
69-
class Login
60+
class RequestPasswordReset
7061
{
7162
}
7263
/**
7364
*/
74-
class Register
65+
class ResetPassword
7566
{
7667
}
7768
/**
7869
*/
79-
class ResetPassword
70+
class VerifyLogin
8071
{
8172
}
8273
namespace App\Actions\Frontend;

app/Actions/Auth/Login.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ public function asController(Request $request)
8585
'device_name' => $deviceName,
8686
], $user));
8787

88-
$deviceName = $request->device_name ?? $user->name . ' device';
89-
9088
if($request->wantsJson())
9189
{
9290
// specifically target mobile devices - LARAVEL SANCTUM

app/Actions/Auth/LoginLinkSent.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Actions\Auth;
4+
5+
use App\Traits\CustomControllerResponsesTrait;
6+
use App\Traits\ThemesTrait;
7+
use Illuminate\Http\Request;
8+
use Lorisleiva\Actions\Concerns\AsAction;
9+
10+
class LoginLinkSent
11+
{
12+
use AsAction;
13+
use ThemesTrait;
14+
use CustomControllerResponsesTrait;
15+
16+
public function asController(Request $request)
17+
{
18+
return $this->generatePage('login-link-sent', 'Auth/LoginLinkSent');
19+
}
20+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Actions\Auth;
4+
5+
use App\Events\CRUDErrorOccurred;
6+
use App\Models\User;
7+
use App\Traits\CustomControllerResponsesTrait;
8+
use App\Traits\ThemesTrait;
9+
use Exception;
10+
use Illuminate\Http\Request;
11+
use Lorisleiva\Actions\Concerns\AsAction;
12+
13+
class LoginViaMagicLink
14+
{
15+
use AsAction;
16+
use ThemesTrait;
17+
use CustomControllerResponsesTrait;
18+
19+
public function asController(Request $request)
20+
{
21+
$request->validate([
22+
'email' => 'required|email'
23+
]);
24+
25+
$user = User::where('email', $request->email)->first();
26+
27+
if ($user) {
28+
try {
29+
$user->sendLoginLink();
30+
} catch (Exception $e) {
31+
ddOnError($e);
32+
33+
event(new CRUDErrorOccurred($e->getMessage()));
34+
35+
return $this->respError(trans('An error occurred while sending the login link'));
36+
}
37+
}
38+
39+
return redirect()->route('login-link-sent')
40+
->withSuccess(trans('We have sent a magic link to the email. If the account exists, you shall receive an email.'));
41+
}
42+
}

app/Actions/Auth/VerifyLogin.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace App\Actions\Auth;
4+
5+
use App\Events\UserLoggedIn;
6+
use App\Models\LoginToken;
7+
use App\Traits\CustomControllerResponsesTrait;
8+
use App\Traits\ThemesTrait;
9+
use Auth;
10+
use Illuminate\Http\Request;
11+
use Lorisleiva\Actions\Concerns\AsAction;
12+
13+
class VerifyLogin
14+
{
15+
use AsAction;
16+
use ThemesTrait;
17+
use CustomControllerResponsesTrait;
18+
19+
public function asController(Request $request, $token)
20+
{
21+
$token = LoginToken::whereToken(md5($token))->firstOrFail();
22+
23+
abort_unless($request->hasValidSignature() && $token->isValid(), 401);
24+
25+
$token->consume();
26+
27+
$user = $token->user;
28+
29+
// verify is user is active, + other checks
30+
if (!$user->canLogin()) {
31+
return redirect()->route('login')->withErrors([
32+
'username' => [trans('auth.account_not_active')],
33+
]);
34+
}
35+
36+
Auth::login($user);
37+
38+
$deviceName = $request->device_name ?? $user->name . ' web device';
39+
40+
event(new UserLoggedIn([
41+
'ip' => $request->ip(),
42+
'user_agent' => $request->header('User-Agent'),
43+
'referer' => $request->header('Referer'),
44+
'host' => $request->header('Host'),
45+
'url' => $request->root(),
46+
'device_name' => $deviceName,
47+
], $user));
48+
49+
$request->session()->regenerate();
50+
51+
if ($user->isAdmin || $user->isSudo) {
52+
return redirect()->route('admin.dashboard');
53+
}
54+
55+
return redirect()->route('user.dashboard');
56+
}
57+
}

app/Mail/MagicLoginLink.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace App\Mail;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Mail\Mailable;
8+
use Illuminate\Mail\Mailables\Content;
9+
use Illuminate\Mail\Mailables\Envelope;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Facades\URL;
12+
use Log;
13+
14+
class MagicLoginLink extends Mailable implements ShouldQueue
15+
{
16+
use Queueable, SerializesModels;
17+
18+
public $plaintextToken;
19+
public $expiresAt;
20+
21+
/**
22+
* Create a new message instance.
23+
*/
24+
public function __construct($plaintextToken, $expiresAt)
25+
{
26+
$this->plaintextToken = $plaintextToken;
27+
$this->expiresAt = $expiresAt;
28+
29+
Log::info('Login token: ' . $this->plaintextToken . ', expiry: ' . $this->expiresAt);
30+
}
31+
32+
/**
33+
* Get the message envelope.
34+
*/
35+
public function envelope(): Envelope
36+
{
37+
return new Envelope(
38+
subject: config('app.name') . ' Login Verification',
39+
);
40+
}
41+
42+
/**
43+
* Get the message content definition.
44+
*/
45+
public function content(): Content
46+
{
47+
return new Content(
48+
markdown: 'emails.magic-login-link',
49+
with: [
50+
'url' => URL::temporarySignedRoute('verify-login', $this->expiresAt, [
51+
'token' => $this->plaintextToken,
52+
]),
53+
],
54+
);
55+
}
56+
57+
/**
58+
* Get the attachments for the message.
59+
*
60+
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
61+
*/
62+
public function attachments(): array
63+
{
64+
return [];
65+
}
66+
}

app/Models/LoginToken.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
9+
class LoginToken extends Model
10+
{
11+
use HasFactory;
12+
13+
protected $fillable = [
14+
'user_id',
15+
'token',
16+
'consumed_at',
17+
'expires_at',
18+
];
19+
20+
protected $casts = [
21+
'expires_at' => 'datetime',
22+
'consumed_at' => 'datetime',
23+
];
24+
25+
public function user(): BelongsTo
26+
{
27+
return $this->belongsTo(User::class);
28+
}
29+
30+
public function isValid() : bool
31+
{
32+
return !$this->isExpired() && !$this->isConsumed();
33+
}
34+
35+
public function isExpired() : bool
36+
{
37+
return $this->expires_at->isBefore(now());
38+
}
39+
40+
public function isConsumed() : bool
41+
{
42+
return $this->consumed_at !== null;
43+
}
44+
45+
public function consume() : void
46+
{
47+
$this->consumed_at = now();
48+
$this->save();
49+
}
50+
}

0 commit comments

Comments
 (0)