view/login.html
Rama main · réplica de lectura del repositorio original.
{{define "content"}}
<form class="auth-card" method="POST" action="{{authURL "/login"}}">
<div class="auth-header">
{{template "logo" .}}
<h1 class="auth-title">{{.ThemeTitle}}</h1>
{{if .ThemeTagline}}<p class="auth-tagline">{{.ThemeTagline}}</p>{{end}}
</div>
{{ .csrfField }}
<input type="hidden" name="state" value="{{.State}}" />
<input type="hidden" name="redirect_uri" value="{{.RedirectURI}}" />
<input type="hidden" name="client_id" value="{{.ClientID}}" />
<input type="hidden" name="scope" value="{{.Scope}}" />
<input type="hidden" name="nonce" value="{{.Nonce}}" />
<input type="hidden" name="code_challenge" value="{{.CodeChallenge}}" />
<input type="hidden" name="code_challenge_method" value="{{.CodeChallengeMethod}}" />
<input type="hidden" name="prompt" value="{{.Prompt}}" />
<input type="hidden" name="max_age" value="{{.MaxAge}}" />
<input type="hidden" name="authorize_sig" value="{{.AuthorizeSig}}" />
{{if not .HideUsernameField}}
<div class="auth-field">
{{if eq .ProfileFieldEmail "is_username"}}
<label class="auth-label" for="username">Email</label>
<input class="auth-input" type="email" id="username" name="username" required autocomplete="{{if eq .PasskeyLoginMode "conditional"}}username webauthn{{else}}email{{end}}" />
{{else}}
<label class="auth-label" for="username">Username</label>
<input class="auth-input" type="text" id="username" name="username" required autocomplete="{{if eq .PasskeyLoginMode "conditional"}}username webauthn{{else}}username{{end}}" />
{{end}}
</div>
{{end}}
{{if not .HidePasswordField}}
<div class="auth-field">
<label class="auth-label" for="password">Password</label>
<input class="auth-input" type="password" id="password" name="password" {{if ne .AuthMode "password_and_passkey"}}required{{end}} autocomplete="current-password" />
</div>
{{end}}
{{if .Error}}
<div class="auth-error" role="alert" id="login-error">{{.Error}}</div>
{{else}}
<div class="auth-error" role="alert" id="login-error" style="display:none"></div>
{{end}}
{{if .ShowLoginButton}}
<button class="auth-btn" type="submit">Log In</button>
{{end}}
{{if and .ShowLoginButton (or (ne .AuthMode "password") .MagicLinkEnabled .FederatedProviders)}}
<div class="auth-divider">or</div>
{{end}}
{{if ne .AuthMode "password"}}
<button type="button" class="{{if .ShowLoginButton}}auth-btn-secondary{{else}}auth-btn{{end}}" id="passkey-login-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="8" cy="8" r="5"/><path d="m13 13 9 9"/><path d="M13 13l-1.5-1.5"/><path d="m16 16 2-2"/><path d="m19 13 2-2"/>
</svg>
Sign in with passkey
</button>
{{end}}
{{if .MagicLinkEnabled}}
<a href="{{authURL "/magic-link"}}?client_id={{.ClientID}}&redirect_uri={{.RedirectURI}}&state={{.State}}&scope={{.Scope}}&nonce={{.Nonce}}&code_challenge={{.CodeChallenge}}&code_challenge_method={{.CodeChallengeMethod}}&prompt={{.Prompt}}&max_age={{.MaxAge}}" class="auth-btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>
Sign in with email link
</a>
{{end}}
{{if .FederatedProviders}}
{{range .FederatedProviders}}
<a href="{{authURL "/federation/"}}{{.ID}}?state={{$.State}}&redirect_uri={{$.RedirectURI}}&client_id={{$.ClientID}}&scope={{$.Scope}}&nonce={{$.Nonce}}&code_challenge={{$.CodeChallenge}}&code_challenge_method={{$.CodeChallengeMethod}}&prompt={{$.Prompt}}&max_age={{$.MaxAge}}" class="auth-btn-secondary">
{{if .HasIcon}}<img src="{{authURL "/federation/"}}{{.ID}}/icon.svg" alt="" width="20" height="20" class="federation-icon" />{{end}}
Sign in with {{.Name}}
</a>
{{end}}
{{end}}
{{if or .AllowSelfSignup (and .SmtpConfigured (not .HidePasswordField))}}
<div class="auth-links">
{{if and .SmtpConfigured (not .HidePasswordField)}}
<div class="auth-link"><a href="{{authURL "/forgot-password"}}?client_id={{.ClientID}}&redirect_uri={{.RedirectURI}}&state={{.State}}&scope={{.Scope}}&nonce={{.Nonce}}&code_challenge={{.CodeChallenge}}&code_challenge_method={{.CodeChallengeMethod}}&prompt={{.Prompt}}&max_age={{.MaxAge}}">Forgot password?</a></div>
{{end}}
{{if .AllowSelfSignup}}
<div class="auth-link" style="margin-left:auto;"><a href="{{.SignupURL}}">Create account</a></div>
{{end}}
</div>
{{end}}
</form>
{{end}}
{{define "scripts"}}
{{if ne .AuthMode "password"}}<script nonce="{{.CspNonce}}" src="{{authURL "/static/passkey.js"}}"></script>{{end}}
<script nonce="{{.CspNonce}}">
var passkeyAbortController = null;
function showPasskeyError(msg) {
const el = document.getElementById('login-error');
el.textContent = msg;
el.style.display = '';
}
async function startPasskeyLogin() {
const username = document.getElementById('username').value.trim();
if (!username) { showPasskeyError('Please enter your {{if eq .ProfileFieldEmail "is_username"}}email{{else}}username{{end}} first.'); return; }
const params = new URLSearchParams({
username,
redirect_uri: document.querySelector('[name=redirect_uri]').value,
state: document.querySelector('[name=state]').value,
client_id: document.querySelector('[name=client_id]').value,
scope: document.querySelector('[name=scope]').value,
nonce: document.querySelector('[name=nonce]').value,
code_challenge: document.querySelector('[name=code_challenge]').value,
code_challenge_method: document.querySelector('[name=code_challenge_method]').value,
prompt: document.querySelector('[name=prompt]').value,
max_age: document.querySelector('[name=max_age]').value,
authorize_sig: document.querySelector('[name=authorize_sig]').value,
});
try {
const beginResp = await fetch('{{authURL "/passkey/login/begin"}}?' + params.toString());
const data = await beginResp.json();
if (!beginResp.ok) { showPasskeyError(data.error || 'Passkey login failed'); return; }
await doPasskeyAuth(data, '{{authURL ""}}');
} catch (e) { showPasskeyError('Passkey error: ' + e.message); }
}
async function startDiscoverableLogin(mediation) {
if (passkeyAbortController) { passkeyAbortController.abort(); }
passkeyAbortController = new AbortController();
var params = new URLSearchParams({
redirect_uri: document.querySelector('[name=redirect_uri]').value,
state: document.querySelector('[name=state]').value,
client_id: document.querySelector('[name=client_id]').value,
scope: document.querySelector('[name=scope]').value,
nonce: document.querySelector('[name=nonce]').value,
code_challenge: document.querySelector('[name=code_challenge]').value,
code_challenge_method: document.querySelector('[name=code_challenge_method]').value,
prompt: document.querySelector('[name=prompt]').value,
max_age: document.querySelector('[name=max_age]').value,
authorize_sig: document.querySelector('[name=authorize_sig]').value,
});
if (mediation === 'conditional') { params.set('mediation', 'conditional'); }
try {
var beginResp = await fetch('{{authURL "/passkey/discoverable/begin"}}?' + params.toString());
var data = await beginResp.json();
if (!beginResp.ok) { showPasskeyError(data.error || 'Passkey login failed'); return; }
await doDiscoverablePasskeyAuth(data, '{{authURL ""}}', mediation, passkeyAbortController.signal);
} catch (e) {
if (e.name !== 'AbortError') { showPasskeyError('Passkey error: ' + e.message); }
}
}
(function() {
var btn = document.getElementById('passkey-login-btn');
{{if eq .PasskeyLoginMode "username_first"}}
if (btn) { btn.addEventListener('click', startPasskeyLogin); }
{{else}}
if (btn) { btn.addEventListener('click', function() { startDiscoverableLogin(); }); }
{{end}}
{{if eq .PasskeyLoginMode "conditional"}}
if (window.PublicKeyCredential && typeof PublicKeyCredential.isConditionalMediationAvailable === 'function') {
PublicKeyCredential.isConditionalMediationAvailable().then(function(available) {
if (available) { startDiscoverableLogin('conditional'); }
});
}
var form = document.querySelector('form.auth-card');
if (form) {
form.addEventListener('submit', function() {
if (passkeyAbortController) { passkeyAbortController.abort(); }
});
}
{{end}}
})();
</script>
{{end}}