2020use CodeIgniter \HTTP \RedirectResponse ;
2121use CodeIgniter \I18n \Time ;
2222use CodeIgniter \Shield \Authentication \Authenticators \Session ;
23+ use CodeIgniter \Shield \Exceptions \InvalidArgumentException ;
2324use CodeIgniter \Shield \Models \LoginModel ;
2425use CodeIgniter \Shield \Models \UserIdentityModel ;
2526use CodeIgniter \Shield \Models \UserModel ;
@@ -103,8 +104,7 @@ public function loginAction()
103104 $ identityModel ->deleteIdentitiesByType ($ user , Session::ID_TYPE_MAGIC_LINK );
104105
105106 // Generate the code and save it as an identity
106- helper ('text ' );
107- $ token = random_string ('crypto ' , 20 );
107+ $ token = $ this ->resolveMode ()['token ' ];
108108
109109 $ identityModel ->insert ([
110110 'user_id ' => $ user ->id ,
@@ -125,9 +125,13 @@ public function loginAction()
125125 $ email = emailer (['mailType ' => 'html ' ])
126126 ->setFrom (setting ('Email.fromEmail ' ), setting ('Email.fromName ' ) ?? '' );
127127 $ email ->setTo ($ user ->email );
128- $ email ->setSubject (lang ('Auth.magicLinkSubject ' ));
128+
129+ $ email ->setSubject ($ this ->resolveMode ()['emailSubject ' ]);
130+
131+ $ emailBodyViewFile = $ this ->resolveMode ()['emailView ' ];
132+
129133 $ email ->setMessage ($ this ->view (
130- setting ('Auth.views ' )[ ' magic-link-email ' ],
134+ config ('Auth ' )-> views [ $ emailBodyViewFile ],
131135 ['token ' => $ token , 'user ' => $ user , 'ipAddress ' => $ ipAddress , 'userAgent ' => $ userAgent , 'date ' => $ date ],
132136 ['debug ' => false ],
133137 ));
@@ -149,7 +153,9 @@ public function loginAction()
149153 */
150154 protected function displayMessage (): string
151155 {
152- return $ this ->view (setting ('Auth.views ' )['magic-link-message ' ]);
156+ $ viewFile = $ this ->resolveMode ()['displayMessageView ' ];
157+
158+ return $ this ->view (config ('Auth ' )->views [$ viewFile ]);
153159 }
154160
155161 /**
@@ -165,20 +171,24 @@ public function verify(): RedirectResponse
165171 throw PageNotFoundException::forPageNotFound ();
166172 }
167173
168- $ token = $ this ->request ->getGet ('token ' );
174+ $ identifier = $ this ->request ->getGet ('token ' );
175+
176+ if ($ this ->request ->is ('post ' )) {
177+ $ identifier = $ this ->request ->getPost ('magicCode ' );
178+ }
169179
170180 /** @var UserIdentityModel $identityModel */
171181 $ identityModel = model (UserIdentityModel::class);
172182
173- $ identity = $ identityModel ->getIdentityBySecret (Session::ID_TYPE_MAGIC_LINK , $ token );
183+ $ identity = $ identityModel ->getIdentityBySecret (Session::ID_TYPE_MAGIC_LINK , $ identifier );
174184
175- $ identifier = $ token ?? '' ;
185+ $ identifier ??= '' ;
176186
177187 // No token found?
178188 if ($ identity === null ) {
179189 $ this ->recordLoginAttempt ($ identifier , false );
180190
181- $ credentials = ['magicLinkToken ' => $ token ];
191+ $ credentials = ['magicLinkToken ' => $ identifier ];
182192 Events::trigger ('failedLogin ' , $ credentials );
183193
184194 return redirect ()->route ('magic-link ' )->with ('error ' , lang ('Auth.magicTokenNotFound ' ));
@@ -191,7 +201,7 @@ public function verify(): RedirectResponse
191201 if (Time::now ()->isAfter ($ identity ->expires )) {
192202 $ this ->recordLoginAttempt ($ identifier , false );
193203
194- $ credentials = ['magicLinkToken ' => $ token ];
204+ $ credentials = ['magicLinkToken ' => $ identifier ];
195205 Events::trigger ('failedLogin ' , $ credentials );
196206
197207 return redirect ()->route ('magic-link ' )->with ('error ' , lang ('Auth.magicLinkExpired ' ));
@@ -254,4 +264,76 @@ protected function getValidationRules(): array
254264 'email ' => config ('Auth ' )->emailValidationRules ,
255265 ];
256266 }
267+
268+ /**
269+ * resolveMode magic-login settings based on the configured mode.
270+ *
271+ * @param string $mode The selected magic login mode (e.g. "clickable", "6-numeric").
272+ *
273+ * @throws InvalidArgumentException
274+ */
275+ protected function resolveMode (?string $ mode = null ): array
276+ {
277+ $ mode ??= config ('Auth ' )->magicLoginMode ;
278+
279+ helper ('text ' );
280+
281+ if ($ mode === 'clickable ' ) {
282+ return [
283+ 'displayMessageView ' => 'magic-link-message ' ,
284+ 'emailView ' => 'magic-link-email ' ,
285+ 'emailSubject ' => lang ('Auth.magicLinkSubject ' ),
286+ 'token ' => random_string ('crypto ' , 20 ),
287+ ];
288+ }
289+
290+ $ parts = explode ('- ' , $ mode , 2 );
291+
292+ if (count ($ parts ) !== 2 ) {
293+ throw new InvalidArgumentException (
294+ "Invalid magic login mode format ' {$ mode }'. Expected format: '<length>-<numeric|alpha|alnum|oneof>' or 'clickable'. " ,
295+ );
296+ }
297+
298+ [$ length , $ type ] = $ parts ;
299+
300+ if (! is_numeric ($ length ) || (int ) $ length <= 0 ) {
301+ throw new InvalidArgumentException (
302+ "Invalid length ' {$ length }' in magic login mode ' {$ mode }'. Must be a positive integer. " ,
303+ );
304+ }
305+
306+ $ length = (int ) $ length ;
307+
308+ return match ($ type ) {
309+ 'numeric ' , 'alpha ' , 'alnum ' => [
310+ 'displayMessageView ' => 'magic-link-code ' ,
311+ 'emailView ' => 'magic-link-email-code ' ,
312+ 'emailSubject ' => lang ('Auth.magicCodeSubject ' ),
313+ 'token ' => random_string ($ type , $ length ),
314+ ],
315+
316+ 'oneof ' => [
317+ 'displayMessageView ' => 'magic-link-code ' ,
318+ 'emailView ' => 'magic-link-email-code ' ,
319+ 'emailSubject ' => lang ('Auth.magicCodeSubject ' ),
320+ 'token ' => $ this ->generateOneofToken ($ length ),
321+ ],
322+
323+ default => throw new InvalidArgumentException ("Invalid magic login mode ' {$ mode }'. Expected format: '<length>-<numeric|alpha|alnum|oneof>'. " ),
324+ };
325+ }
326+
327+ /**
328+ * Generate a token by picking ONE of the fixed patterns: numeric, alpha, alnum.
329+ */
330+ private function generateOneofToken (int $ length ): string
331+ {
332+ helper ('text ' );
333+
334+ $ patterns = ['numeric ' , 'alpha ' , 'alnum ' ];
335+ $ chosenMode = $ patterns [array_rand ($ patterns )];
336+
337+ return random_string ($ chosenMode , $ length );
338+ }
257339}
0 commit comments