feat(external): permit login via email (magic link)
This commit is contained in:
@@ -183,6 +183,36 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Helper text above the magic-link form ("No password? Enter your
|
||||
email…"). Quieter visual weight than the form labels. */
|
||||
.auth-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Status banner under the magic-link form. Uniform anti-enumeration
|
||||
message rendered on every successful 2xx; error variant only used
|
||||
for the 503-not-configured branch or network failures. */
|
||||
.auth-status {
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.auth-status-success {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text);
|
||||
border-left: 3px solid var(--color-warning-orange-text);
|
||||
}
|
||||
.auth-status-error {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text);
|
||||
border-left: 3px solid var(--color-warning-orange-text);
|
||||
}
|
||||
|
||||
/* Divider between password and SSO login */
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
|
||||
@@ -1072,6 +1072,54 @@ if (isLoginPage && registerForm) {
|
||||
}
|
||||
|
||||
// Admin setup form submission
|
||||
// Magic-link form: anti-enumeration sign-in by email.
|
||||
// The server always responds 200 with a uniform message when SMTP is
|
||||
// configured, regardless of whether the email maps to an account or
|
||||
// whether that account is eligible for magic-link sign-in. The UI
|
||||
// mirrors that — same success state for every successful 2xx — so
|
||||
// the page can't be used as an oracle. 503 is the one exception
|
||||
// (SMTP not configured); operators need to see it.
|
||||
const magicLinkForm = /** @type {HTMLFormElement | null} */ (document.getElementById('magic-link-form'));
|
||||
const magicLinkStatus = document.getElementById('magic-link-status');
|
||||
const magicLinkSubmit = /** @type {HTMLButtonElement | null} */ (document.getElementById('magic-link-submit'));
|
||||
if (isLoginPage && magicLinkForm && magicLinkStatus) {
|
||||
magicLinkForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const email = inputVal('magic-link-email');
|
||||
if (!email) return;
|
||||
|
||||
magicLinkStatus.className = 'auth-status hidden';
|
||||
magicLinkStatus.textContent = '';
|
||||
if (magicLinkSubmit) magicLinkSubmit.disabled = true;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/auth/magic-link/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
if (resp.status === 503) {
|
||||
magicLinkStatus.className = 'auth-status auth-status-error';
|
||||
magicLinkStatus.textContent = i18n.t('auth.magicLinkUnavailable', 'Sign-in by email is not available on this server.');
|
||||
return;
|
||||
}
|
||||
// Any 2xx → uniform success message regardless of whether
|
||||
// the server actually queued a mail. Anti-enumeration.
|
||||
magicLinkStatus.className = 'auth-status auth-status-success';
|
||||
magicLinkStatus.textContent = i18n.t('auth.magicLinkSent', 'If an account exists for that email, a sign-in link has been sent. Check your inbox.');
|
||||
magicLinkForm.reset();
|
||||
} catch (err) {
|
||||
magicLinkStatus.className = 'auth-status auth-status-error';
|
||||
magicLinkStatus.textContent = i18n.t('auth.magicLinkNetworkError', {
|
||||
message: /** @type {Error} */ (err).message
|
||||
});
|
||||
} finally {
|
||||
if (magicLinkSubmit) magicLinkSubmit.disabled = false;
|
||||
magicLinkStatus.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoginPage && adminSetupForm) {
|
||||
adminSetupForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -412,7 +412,14 @@
|
||||
"admin_create_error": "Error creating administrator account",
|
||||
"or": "or",
|
||||
"sso_login": "Sign in with SSO",
|
||||
"sso_login_provider": "Sign in with {{provider}}"
|
||||
"sso_login_provider": "Sign in with {{provider}}",
|
||||
"magicLinkHint": "No password? Enter your email and we'll send you a one-time sign-in link.",
|
||||
"magicLinkEmailLabel": "Email address",
|
||||
"magicLinkEmailPlaceholder": "you@example.com",
|
||||
"magicLinkSubmit": "Send sign-in link",
|
||||
"magicLinkSent": "If an account exists for that email, a sign-in link has been sent. Check your inbox.",
|
||||
"magicLinkUnavailable": "Sign-in by email is not available on this server.",
|
||||
"magicLinkNetworkError": "Could not reach the server: {{message}}"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Storage",
|
||||
|
||||
@@ -412,7 +412,14 @@
|
||||
"admin_create_error": "Erreur lors de la création du compte administrateur",
|
||||
"or": "ou",
|
||||
"sso_login": "Se connecter avec SSO",
|
||||
"sso_login_provider": "Se connecter avec {{provider}}"
|
||||
"sso_login_provider": "Se connecter avec {{provider}}",
|
||||
"magicLinkHint": "Pas de mot de passe ? Saisissez votre adresse e-mail et nous vous enverrons un lien de connexion à usage unique.",
|
||||
"magicLinkEmailLabel": "Adresse e-mail",
|
||||
"magicLinkEmailPlaceholder": "vous@exemple.com",
|
||||
"magicLinkSubmit": "Envoyer le lien de connexion",
|
||||
"magicLinkSent": "Si un compte existe pour cette adresse, un lien de connexion vient d'être envoyé. Consultez votre boîte de réception.",
|
||||
"magicLinkUnavailable": "La connexion par e-mail n'est pas disponible sur ce serveur.",
|
||||
"magicLinkNetworkError": "Impossible de joindre le serveur : {{message}}"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Stockage",
|
||||
|
||||
@@ -108,6 +108,42 @@
|
||||
<span data-i18n="auth.sso_login">Sign in with SSO</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sign-in by email link (magic-link). For users without a
|
||||
password (external recipients invited via share, future
|
||||
passwordless-only accounts). Always rendered — the
|
||||
endpoint is anti-enumeration so showing it doesn't leak
|
||||
anything; if SMTP isn't configured the server returns
|
||||
503 and we show a generic "unavailable" message. -->
|
||||
<div id="magic-link-section">
|
||||
<div class="auth-divider">
|
||||
<span data-i18n="auth.or">or</span>
|
||||
</div>
|
||||
<p class="auth-hint" data-i18n="auth.magicLinkHint">
|
||||
No password? Enter your email and we'll send you a one-time sign-in link.
|
||||
</p>
|
||||
<form class="auth-form" id="magic-link-form">
|
||||
<div class="auth-input-group">
|
||||
<label class="auth-label" for="magic-link-email" data-i18n="auth.magicLinkEmailLabel">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="magic-link-email"
|
||||
class="auth-input"
|
||||
name="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="you@example.com"
|
||||
data-i18n-placeholder="auth.magicLinkEmailPlaceholder"
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="auth-button" id="magic-link-submit" data-i18n="auth.magicLinkSubmit">
|
||||
Send sign-in link
|
||||
</button>
|
||||
</form>
|
||||
<div id="magic-link-status" class="auth-status hidden" role="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-toggle">
|
||||
<span data-i18n="auth.no_account">Don't have an account?</span>
|
||||
|
||||
Reference in New Issue
Block a user