Well i did it. Not a receipe but a working case.
Some precisions
Don’t forget settings
Email transport settings are required in site/config/config.php in order to send the login code.
Behavior
There is two mode:
Default is password.
I have a custom attribute data-mode
on <form>
which reflect that mode for design purpose. I hide or show inputs depending on mode.
Give new users a link to passwordless mode
I have a trigger which send a welcome mail with link to new users for login purpose. I add a #passwordless
hash to url. That way they come to the form with passwordless mode enabled. Later they can add a password.
Notifications
I have a .error
area where i notice user errors.
I have a .info
area where i notice an email was send.
Final automatic redirection
I have an automatic redirection. Users with panel access are redirected to panel. Other are redirected to the page they come from. I use both url param and hidden field for that purpose. The url param is added to <a>
targetting the login page in menu (an other script). Then i use the hidden field cause the login page can be call multiple times (user errors, of passwordless mode).
Code
Controller:
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Exception\NotFoundException;
use Kirby\Exception\PermissionException;
return function ($kirby) {
$error = false;
$errorText = "";
$info = "";
$redirection = get('redirection') ?? get('location');
$mode = get('mode');
$codeSent = false;
function openNextPage($user, $redirection)
{
if ($user->role()->permissions()->for('access', 'panel')) {
go($user->site()->panelUrl());
} else if ($redirection) {
go($redirection);
} else {
go('/');
}
}
// don't show the login screen to already logged in users
if ($user = $kirby->user()) {
openNextPage($user, $redirection);
}
// handle the form submission
if ($kirby->request()->is('POST') && get('email')) {
if (get('mode') === "passwordless") {
if ($verification_code = get('verification_code')) {
$codeSent = true;
try {
$user = $kirby->auth()->verifyChallenge($verification_code);
openNextPage($user, $redirection);
} catch (LogicException $exception) {
$error = true;
$errorText = "Le challenge d'authentification est invalid.";
} catch (NotFoundException $exception) {
$error = true;
$errorText = "L'utilisateur n'existe pas.";
} catch (PermissionException $exception) {
$error = true;
$errorText = "Le code est erroné ou périmé.";
} catch (InvalidArgumentException $exception) {
$error = true;
$errorText = "Aucun challenge d'authentification n'est actif.";
}
} else {
try {
$status = $kirby->auth()->createChallenge(get('email'), false, 'login');
$codeSent = true;
$info = "Un code d'identification vient de vous être envoyé par email.";
} catch (LogicException $exception) {
$error = true;
$errorText = "Il n'y a pas de challenge d'authentification approprié.";
} catch (NotFoundException $exception) {
$error = true;
$errorText = "L'utilisateur n'existe pas.";
} catch (PermissionException $exception) {
$error = true;
$errorText = "Vous avez dépassé le nombre de tentatives autorisé.";
}
}
} else if (get('mode') === "password") {
try {
$user = $kirby->auth()->login(get('email'), get('password'), false);
openNextPage($user, $redirection);
} catch (NotFoundException $exception) {
$error = true;
$errorText = "L'utilisateur n'existe pas.";
} catch (PermissionException $exception) {
$error = true;
$errorText = "Vous avez dépassé le nombre de tentatives autorisé.";
} catch (InvalidArgumentException $exception) {
$error = true;
$errorText = "Mot de passe erroné.";
}
}
} else {
$error = false;
}
return compact('error', 'errorText', 'mode', 'codeSent', 'info', 'redirection');
};
Template:
<?php snippet('header') ?>
<div id="login">
<form method="post" class="card" data-mode="<?= $mode ?>" action="<?= $page->url() ?>">
<button type="button" class="mode_button password_mode" onclick='setMode("passwordless")'>Se connecter sans mot de passe</button>
<button type="button" class="mode_button passwordless_mode" onclick='setMode("password")'>Se connecter avec un mot de passe</button>
<?php if ($error) : ?>
<div class="error"><?= $errorText ?? $page->alert()->html() ?></div>
<?php endif ?>
<?php if ($info) : ?>
<div class="info"><?= $info ?></div>
<?php endif ?>
<div>
<label for="email"><?= $page->login()->html() ?></label>
<input type="email" id="email" name="email" placeholder="nom@exemple.com" value="<?= esc(get('email')) ?>">
</div>
<div class="password_mode">
<label for="password"><?= $page->password()->html() ?></label>
<input type="password" id="password" name="password" placeholder="************" value="<?= esc(get('password')) ?>">
</div>
<?php if ($codeSent) : ?>
<div class="passwordless_mode">
<label for="verification_code"><?= $page->verification_code()->html() ?></label>
<input type="text" id="verification_code" name="verification_code" placeholder="123 456" value="<?= esc(get('verification_code')) ?>">
</div>
<?php endif ?>
<input type="hidden" id="redirection" name="redirection" value="<?= esc($redirection) ?>" />
<input type="hidden" id="mode" name="mode" value="<?= esc(get('mode')) ?>" />
<div class="password_mode">
<input type="submit" name="submit_login" value="<?= $page->login_button()->html() ?>">
</div>
<div class="passwordless_mode">
<?php if ($codeSent) : ?>
<input type="submit" name="submit_passwordless" value="<?= $page->login_passwordless_button()->html() ?>">
<?php else : ?>
<input type="submit" name="submit_passwordless" value="<?= $page->get_code_button()->html() ?>">
<?php endif ?>
</div>
</form>
<script>
if (window.location.hash === "#passwordless" || document.querySelector("#mode").value === "passwordless") {
setMode("passwordless");
} else {
setMode("password");
}
function setMode(mode) {
window.location.hash = '#' + mode;
document.querySelector("#login form").dataset.mode = mode;
document.querySelector("#login #mode").value = mode;
}
document.querySelectorAll("form [type=submit]").forEach(button => {
button.addEventListener("click", event => {
event.currentTarget.toggleAttribute("disabled");
event.currentTarget.value = "Veuillez patienter...";
})
})
</script>
</div>
<?php snippet('footer') ?>
CSS:
#login [data-mode="password"] .passwordless_mode,
#login :not([data-mode]) .passwordless_mode {
display: none;
}
#login [data-mode="passwordless"] .password_mode {
display: none;
}
[type="submit"]:disabled {
background-color: grey !important;
pointer-events: none;
}
If you like it … use it