• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 23341393117

20 Mar 2026 11:45AM UTC coverage: 64.989% (+1.2%) from 63.745%
23341393117

push

github

daycry
Merge branch 'development' of https://github.com/daycry/auth into development

4 of 4 new or added lines in 2 files covered. (100.0%)

315 existing lines in 13 files now uncovered.

3306 of 5087 relevant lines covered (64.99%)

47.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Controllers/MagicLinkController.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Controllers;
15

16
use CodeIgniter\Events\Events;
17
use CodeIgniter\Exceptions\RuntimeException;
18
use CodeIgniter\HTTP\RedirectResponse;
19
use CodeIgniter\HTTP\ResponseInterface;
20
use CodeIgniter\I18n\Time;
21
use Daycry\Auth\Authentication\Authenticators\Session;
22
use Daycry\Auth\Libraries\TokenEmailSender;
23
use Daycry\Auth\Models\UserIdentityModel;
24
use Daycry\Auth\Models\UserModel;
25

26
/**
27
 * Handles "Magic Link" logins - an email-based
28
 * no-password login protocol. This works much
29
 * like password reset would, but Shield provides
30
 * this in place of password reset. It can also
31
 * be used on it's own without an email/password
32
 * login strategy.
33
 */
34
class MagicLinkController extends BaseAuthController
35
{
36
    /**
37
     * @var UserModel
38
     */
39
    protected $provider;
40

41
    public function __construct()
×
42
    {
43
        /** @var class-string<UserModel> $providerClass */
44
        $providerClass = setting('Auth.userProvider');
×
45

46
        $this->provider = new $providerClass();
×
47
    }
48

49
    /**
50
     * Displays the view to enter their email address
51
     * so an email can be sent to them.
52
     */
53
    public function loginView(): ResponseInterface
×
54
    {
55
        if (! setting('AuthSecurity.allowMagicLinkLogins')) {
×
56
            return $this->handleError(
×
57
                config('Auth')->loginRoute(),
×
58
                lang('Auth.magicLinkDisabled'),
×
59
            );
×
60
        }
61

62
        if (($redirect = $this->redirectIfLoggedIn()) instanceof RedirectResponse) {
×
63
            return $redirect;
×
64
        }
65

66
        $content = $this->view(setting('Auth.views')['magic-link-login']);
×
67

68
        return $this->response->setBody($content);
×
69
    }
70

71
    /**
72
     * Receives the email from the user, creates the hash
73
     * to a user identity, and sends an email to the given
74
     * email address.
75
     */
76
    public function loginAction(): RedirectResponse
×
77
    {
78
        if (! setting('AuthSecurity.allowMagicLinkLogins')) {
×
79
            return $this->handleError(
×
80
                config('Auth')->loginRoute(),
×
81
                lang('Auth.magicLinkDisabled'),
×
82
            );
×
83
        }
84

85
        // Validate email format
86
        $rules    = $this->getValidationRules();
×
87
        $postData = $this->request->getPost();
×
88

89
        if (! $this->validateRequest($postData, $rules)) {
×
90
            return $this->handleValidationError('magic-link');
×
91
        }
92

93
        // Check if the user exists
94
        $email = $this->request->getPost('email');
×
95
        $user  = $this->provider->findByCredentials(['email' => $email]);
×
96

97
        if ($user === null) {
×
98
            return $this->handleError('magic-link', lang('Auth.invalidEmail'));
×
99
        }
100

UNCOV
101
        $sender = new TokenEmailSender();
×
102

103
        try {
UNCOV
104
            $sender->sendTokenEmail(
×
105
                $user,
×
UNCOV
106
                Session::ID_TYPE_MAGIC_LINK,
×
UNCOV
107
                setting('AuthSecurity.magicLinkLifetime'),
×
108
                lang('Auth.magicLinkSubject'),
×
109
                setting('Auth.views')['magic-link-email'],
×
UNCOV
110
            );
×
111
        } catch (RuntimeException $e) {
×
112
            return $this->handleError('magic-link', lang('Auth.unableSendEmailToUser', [$user->email]));
×
113
        }
114

115
        // Redirect to message page instead of returning the view directly
116
        return redirect()->route('magic-link-message');
×
117
    }
118

119
    /**
120
     * Display the "What's happening/next" message to the user.
121
     */
122
    protected function displayMessage(): string
×
123
    {
UNCOV
124
        return $this->view(setting('Auth.views')['magic-link-message']);
×
125
    }
126

127
    /**
128
     * Shows the message view (public route)
129
     */
130
    public function messageView(): ResponseInterface
×
131
    {
132
        $content = $this->view(setting('Auth.views')['magic-link-message']);
×
133

134
        return $this->response->setBody($content);
×
135
    }
136

137
    /**
138
     * Handles the GET request from the email
139
     */
140
    public function verify(): RedirectResponse
×
141
    {
UNCOV
142
        if (! setting('AuthSecurity.allowMagicLinkLogins')) {
×
UNCOV
143
            return redirect()->route('login')->with('error', lang('Auth.magicLinkDisabled'));
×
144
        }
145

UNCOV
146
        $token = $this->request->getGet('token');
×
147

148
        /** @var UserIdentityModel $identityModel */
UNCOV
149
        $identityModel = model(UserIdentityModel::class);
×
150

UNCOV
151
        $identity = $identityModel->getIdentityBySecret(Session::ID_TYPE_MAGIC_LINK, $token);
×
152

153
        $identifier = $token ?? '';
×
154

155
        // No token found?
UNCOV
156
        if ($identity === null) {
×
UNCOV
157
            $this->recordLoginAttempt(Session::ID_TYPE_MAGIC_LINK, $identifier, false);
×
158

UNCOV
159
            $credentials = ['magicLinkToken' => $token];
×
UNCOV
160
            Events::trigger('failedLogin', $credentials);
×
161

UNCOV
162
            return redirect()->route('magic-link')->with('error', lang('Auth.magicTokenNotFound'));
×
163
        }
164

165
        // Delete the db entry so it cannot be used again.
UNCOV
166
        $identityModel->delete($identity->id);
×
167

168
        // Token expired?
UNCOV
169
        if (Time::now()->isAfter($identity->expires)) {
×
UNCOV
170
            $this->recordLoginAttempt(Session::ID_TYPE_MAGIC_LINK, $identifier, false);
×
171

UNCOV
172
            $credentials = ['magicLinkToken' => $token];
×
173
            Events::trigger('failedLogin', $credentials);
×
174

UNCOV
175
            return redirect()->route('magic-link')->with('error', lang('Auth.magicLinkExpired'));
×
176
        }
177

178
        /** @var Session $authenticator */
UNCOV
179
        $authenticator = auth('session')->getAuthenticator();
×
180

181
        // If an action has been defined
182
        if ($authenticator->hasAction($identity->user_id)) {
×
UNCOV
183
            return redirect()->route('auth-action-show')->with('error', lang('Auth.needActivate'));
×
184
        }
185

186
        // Log the user in
187
        $authenticator->loginById($identity->user_id);
×
188

UNCOV
189
        $user = $authenticator->getUser();
×
190

191
        $this->recordLoginAttempt(Session::ID_TYPE_MAGIC_LINK, $identifier, true, $user->id);
×
192

193
        // Give the developer a way to know the user
194
        // logged in via a magic link.
UNCOV
195
        session()->setTempdata('magicLogin', true);
×
196

197
        Events::trigger('magicLogin');
×
198

199
        // Get our login redirect url
200
        return redirect()->to(config('Auth')->loginRedirect());
×
201
    }
202

203
    /**
204
     * Returns the rules that should be used for validation.
205
     *
206
     * @return         array<string, array<string, list<string>|string>>
207
     * @phpstan-return array<string, array<string, string|list<string>>>
208
     */
UNCOV
209
    protected function getValidationRules(): array
×
210
    {
UNCOV
211
        return [
×
UNCOV
212
            'email' => config('Auth')->emailValidationRules,
×
213
        ];
×
214
    }
215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc