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

codeigniter4 / shield / 6132899896

09 Sep 2023 08:02PM UTC coverage: 92.601% (+0.05%) from 92.549%
6132899896

push

github

web-flow
Merge pull request #811 from kenjis/reuse-workflows

chore: use workflows in codeigniter4/.github

2115 of 2284 relevant lines covered (92.6%)

49.05 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace CodeIgniter\Shield\Controllers;
6

7
use App\Controllers\BaseController;
8
use CodeIgniter\Events\Events;
9
use CodeIgniter\HTTP\IncomingRequest;
10
use CodeIgniter\HTTP\RedirectResponse;
11
use CodeIgniter\I18n\Time;
12
use CodeIgniter\Shield\Authentication\Authenticators\Session;
13
use CodeIgniter\Shield\Models\LoginModel;
14
use CodeIgniter\Shield\Models\UserIdentityModel;
15
use CodeIgniter\Shield\Models\UserModel;
16
use CodeIgniter\Shield\Traits\Viewable;
17

18
/**
19
 * Handles "Magic Link" logins - an email-based
20
 * no-password login protocol. This works much
21
 * like password reset would, but Shield provides
22
 * this in place of password reset. It can also
23
 * be used on it's own without an email/password
24
 * login strategy.
25
 */
26
class MagicLinkController extends BaseController
27
{
28
    use Viewable;
29

30
    /**
31
     * @var UserModel
32
     */
33
    protected $provider;
34

35
    public function __construct()
36
    {
37
        helper('setting');
28✔
38

39
        /** @var class-string<UserModel> $providerClass */
40
        $providerClass = setting('Auth.userProvider');
28✔
41

42
        $this->provider = new $providerClass();
28✔
43
    }
44

45
    /**
46
     * Displays the view to enter their email address
47
     * so an email can be sent to them.
48
     *
49
     * @return RedirectResponse|string
50
     */
51
    public function loginView()
52
    {
53
        if (! setting('Auth.allowMagicLinkLogins')) {
8✔
54
            return redirect()->route('login')->with('error', lang('Auth.magicLinkDisabled'));
2✔
55
        }
56

57
        if (auth()->loggedIn()) {
6✔
58
            return redirect()->to(config('Auth')->loginRedirect());
2✔
59
        }
60

61
        return $this->view(setting('Auth.views')['magic-link-login']);
4✔
62
    }
63

64
    /**
65
     * Receives the email from the user, creates the hash
66
     * to a user identity, and sends an email to the given
67
     * email address.
68
     *
69
     * @return RedirectResponse|string
70
     */
71
    public function loginAction()
72
    {
73
        if (! setting('Auth.allowMagicLinkLogins')) {
10✔
74
            return redirect()->route('login')->with('error', lang('Auth.magicLinkDisabled'));
2✔
75
        }
76

77
        // Validate email format
78
        $rules = $this->getValidationRules();
8✔
79
        if (! $this->validateData($this->request->getPost(), $rules, [], config('Auth')->DBGroup)) {
8✔
80
            return redirect()->route('magic-link')->with('errors', $this->validator->getErrors());
4✔
81
        }
82

83
        // Check if the user exists
84
        $email = $this->request->getPost('email');
4✔
85
        $user  = $this->provider->findByCredentials(['email' => $email]);
4✔
86

87
        if ($user === null) {
4✔
88
            return redirect()->route('magic-link')->with('error', lang('Auth.invalidEmail'));
2✔
89
        }
90

91
        /** @var UserIdentityModel $identityModel */
92
        $identityModel = model(UserIdentityModel::class);
2✔
93

94
        // Delete any previous magic-link identities
95
        $identityModel->deleteIdentitiesByType($user, Session::ID_TYPE_MAGIC_LINK);
2✔
96

97
        // Generate the code and save it as an identity
98
        helper('text');
2✔
99
        $token = random_string('crypto', 20);
2✔
100

101
        $identityModel->insert([
2✔
102
            'user_id' => $user->id,
2✔
103
            'type'    => Session::ID_TYPE_MAGIC_LINK,
2✔
104
            'secret'  => $token,
2✔
105
            'expires' => Time::now()->addSeconds(setting('Auth.magicLinkLifetime'))->format('Y-m-d H:i:s'),
2✔
106
        ]);
2✔
107

108
        /** @var IncomingRequest $request */
109
        $request = service('request');
2✔
110

111
        $ipAddress = $request->getIPAddress();
2✔
112
        $userAgent = (string) $request->getUserAgent();
2✔
113
        $date      = Time::now()->toDateTimeString();
2✔
114

115
        // Send the user an email with the code
116
        $email = emailer()->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? '');
2✔
117
        $email->setTo($user->email);
2✔
118
        $email->setSubject(lang('Auth.magicLinkSubject'));
2✔
119
        $email->setMessage($this->view(setting('Auth.views')['magic-link-email'], ['token' => $token, 'ipAddress' => $ipAddress, 'userAgent' => $userAgent, 'date' => $date]));
2✔
120

121
        if ($email->send(false) === false) {
2✔
122
            log_message('error', $email->printDebugger(['headers']));
×
123

124
            return redirect()->route('magic-link')->with('error', lang('Auth.unableSendEmailToUser', [$user->email]));
×
125
        }
126

127
        // Clear the email
128
        $email->clear();
2✔
129

130
        return $this->displayMessage();
2✔
131
    }
132

133
    /**
134
     * Display the "What's happening/next" message to the user.
135
     */
136
    protected function displayMessage(): string
137
    {
138
        return $this->view(setting('Auth.views')['magic-link-message']);
2✔
139
    }
140

141
    /**
142
     * Handles the GET request from the email
143
     */
144
    public function verify(): RedirectResponse
145
    {
146
        if (! setting('Auth.allowMagicLinkLogins')) {
10✔
147
            return redirect()->route('login')->with('error', lang('Auth.magicLinkDisabled'));
2✔
148
        }
149

150
        $token = $this->request->getGet('token');
8✔
151

152
        /** @var UserIdentityModel $identityModel */
153
        $identityModel = model(UserIdentityModel::class);
8✔
154

155
        $identity = $identityModel->getIdentityBySecret(Session::ID_TYPE_MAGIC_LINK, $token);
8✔
156

157
        $identifier = $token ?? '';
8✔
158

159
        // No token found?
160
        if ($identity === null) {
8✔
161
            $this->recordLoginAttempt($identifier, false);
2✔
162

163
            $credentials = ['magicLinkToken' => $token];
2✔
164
            Events::trigger('failedLogin', $credentials);
2✔
165

166
            return redirect()->route('magic-link')->with('error', lang('Auth.magicTokenNotFound'));
2✔
167
        }
168

169
        // Delete the db entry so it cannot be used again.
170
        $identityModel->delete($identity->id);
6✔
171

172
        // Token expired?
173
        if (Time::now()->isAfter($identity->expires)) {
6✔
174
            $this->recordLoginAttempt($identifier, false);
2✔
175

176
            $credentials = ['magicLinkToken' => $token];
2✔
177
            Events::trigger('failedLogin', $credentials);
2✔
178

179
            return redirect()->route('magic-link')->with('error', lang('Auth.magicLinkExpired'));
2✔
180
        }
181

182
        /** @var Session $authenticator */
183
        $authenticator = auth('session')->getAuthenticator();
4✔
184

185
        // If an action has been defined
186
        if ($authenticator->hasAction($identity->user_id)) {
4✔
187
            return redirect()->route('auth-action-show')->with('error', lang('Auth.needActivate'));
2✔
188
        }
189

190
        // Log the user in
191
        $authenticator->loginById($identity->user_id);
2✔
192

193
        $user = $authenticator->getUser();
2✔
194

195
        $this->recordLoginAttempt($identifier, true, $user->id);
2✔
196

197
        // Give the developer a way to know the user
198
        // logged in via a magic link.
199
        session()->setTempdata('magicLogin', true);
2✔
200

201
        Events::trigger('magicLogin');
2✔
202

203
        // Get our login redirect url
204
        return redirect()->to(config('Auth')->loginRedirect());
2✔
205
    }
206

207
    /**
208
     * @param int|string|null $userId
209
     */
210
    private function recordLoginAttempt(
211
        string $identifier,
212
        bool $success,
213
        $userId = null
214
    ): void {
215
        /** @var LoginModel $loginModel */
216
        $loginModel = model(LoginModel::class);
6✔
217

218
        $loginModel->recordLoginAttempt(
6✔
219
            Session::ID_TYPE_MAGIC_LINK,
6✔
220
            $identifier,
6✔
221
            $success,
6✔
222
            $this->request->getIPAddress(),
6✔
223
            (string) $this->request->getUserAgent(),
6✔
224
            $userId
6✔
225
        );
6✔
226
    }
227

228
    /**
229
     * Returns the rules that should be used for validation.
230
     *
231
     * @return array<string, array<string, array<string>|string>>
232
     * @phpstan-return array<string, array<string, string|list<string>>>
233
     */
234
    protected function getValidationRules(): array
235
    {
236
        return [
8✔
237
            'email' => [
8✔
238
                'label' => 'Auth.email',
8✔
239
                'rules' => config('AuthSession')->emailValidationRules,
8✔
240
            ],
8✔
241
        ];
8✔
242
    }
243
}
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