mirror of
https://github.com/sol1/rustguac.git
synced 2026-10-25 21:19:45 +00:00
f3081d4c88
Since 1.12.0 a login whose email the provider marks unverified is refused. authentik's default email scope mapping sends email_verified: False for every user, so every authentik login has been refused since then, and there was no way round it in rustguac. The authentik setup guide gets a step for an email mapping that sends true, with the caveat that users must not be able to change their own email. For providers that can't be changed, [oidc] allow_unverified_email = true lets these logins through, with a warning at startup. The refusal's log line now says what to change, and the login page asks the user to see an admin.
225 lines
9.8 KiB
HTML
225 lines
9.8 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>rustguac</title>
|
|
<link rel="stylesheet" href="/rustguac.css">
|
|
<style>
|
|
body { display: flex; flex-direction: column; align-items: center; }
|
|
.login-container { max-width: 380px; width: 100%; margin: var(--s-4) 0; }
|
|
#sso-section { display: none; margin-bottom: var(--s-4); }
|
|
#sso-btn {
|
|
width: 100%;
|
|
height: var(--ctl-lg);
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: var(--bg);
|
|
font-size: var(--fz-md);
|
|
font-weight: bold;
|
|
}
|
|
#sso-btn:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
#sso-btn:disabled, #sso-btn:disabled:hover {
|
|
background: var(--surface);
|
|
border-color: var(--border);
|
|
color: var(--text-muted);
|
|
cursor: not-allowed;
|
|
}
|
|
#sso-error {
|
|
margin-top: var(--s-2);
|
|
font-size: var(--fz-sm);
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
}
|
|
#sso-error:empty { display: none; }
|
|
.api-key-toggle {
|
|
color: var(--text-muted);
|
|
font-size: var(--fz-sm);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
margin-top: var(--s-3);
|
|
display: none;
|
|
}
|
|
.api-key-toggle:hover { color: var(--text); }
|
|
.api-key-toggle .chevron {
|
|
display: inline-block;
|
|
transition: transform .2s;
|
|
margin-right: var(--s-1);
|
|
}
|
|
.api-key-toggle .chevron.open { transform: rotate(90deg); }
|
|
#login-form {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
padding: var(--s-4);
|
|
border-radius: var(--radius);
|
|
margin-top: var(--s-2);
|
|
}
|
|
#login-form input {
|
|
display: block;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
margin-top: var(--s-1);
|
|
}
|
|
#login-form button {
|
|
margin-top: var(--s-4);
|
|
width: 100%;
|
|
height: var(--ctl-lg);
|
|
font-size: var(--fz-md);
|
|
}
|
|
</style>
|
|
<script>(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k]);if(o.bg_pattern&&o.bg_pattern!=='none'){var s=document.createElement('style');s.id='bg-pattern-style';s.textContent='body{background-image:'+o.bg_pattern+';background-attachment:fixed}';document.head.appendChild(s)}}catch(e){}}})();</script>
|
|
</head>
|
|
<body>
|
|
<div class="brand-row">
|
|
<img id="site-logo" src="/logo.svg" alt="">
|
|
<h1>rustguac</h1>
|
|
</div>
|
|
|
|
<div class="login-container">
|
|
<div id="sso-section">
|
|
<button id="sso-btn">Sign in with SSO</button>
|
|
<div id="sso-error" role="alert"></div>
|
|
</div>
|
|
|
|
<div class="api-key-toggle" id="api-key-toggle">
|
|
<span class="chevron" id="api-key-chevron">▶</span> Sign in with API key
|
|
</div>
|
|
|
|
<form id="login-form">
|
|
<strong>Login</strong>
|
|
<label>API Key
|
|
<input type="password" id="api-key" placeholder="Bearer API key" required>
|
|
</label>
|
|
<button type="submit" id="login-btn">Login</button>
|
|
<div id="error"></div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// Messages shown under the SSO button. Keys other than `unavailable`
|
|
// match the sso_error codes the OIDC callback redirects with.
|
|
var ssoMessages = {
|
|
failed: 'SSO login failed — please try again.',
|
|
no_account: 'You do not have an account here. Ask an administrator to add you.',
|
|
account_mismatch: 'This sign-in does not match the account used here before. Ask an administrator.',
|
|
email_unverified: 'Your identity provider says your email address is not verified. Ask an administrator.',
|
|
disabled: 'Your account is disabled. Contact an administrator.',
|
|
unavailable: 'SSO provider is temporarily unavailable — retrying…'
|
|
};
|
|
var ssoErrorEl = document.getElementById('sso-error');
|
|
|
|
// Show SSO error if redirected back from a failed callback
|
|
var ssoError = new URLSearchParams(window.location.search).get('sso_error');
|
|
if (ssoError) {
|
|
ssoErrorEl.textContent = Object.prototype.hasOwnProperty.call(ssoMessages, ssoError)
|
|
? ssoMessages[ssoError] : ssoMessages.failed;
|
|
history.replaceState(null, '', '/');
|
|
}
|
|
|
|
// If already logged in via API key, redirect
|
|
if (sessionStorage.getItem('rustguac_api_key')) {
|
|
window.location.href = '/connections.html';
|
|
}
|
|
|
|
// Check if already authenticated via cookie
|
|
fetch('/api/me', { credentials: 'same-origin' })
|
|
.then(function(res) {
|
|
if (res.ok) window.location.href = '/connections.html';
|
|
});
|
|
|
|
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);var s=document.getElementById('bg-pattern-style');if(!s){s=document.createElement('style');s.id='bg-pattern-style';document.head.appendChild(s)}s.textContent=colors.bg_pattern&&colors.bg_pattern!=='none'?'body{background-image:'+colors.bg_pattern+';background-attachment:fixed}':'';localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
|
|
var _themePresets={},_adminPreset='aurora';
|
|
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'aurora';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){if(l.src!==t.logo_url&&!l.src.endsWith(t.logo_url))l.src=t.logo_url;l.style.display=''}}}
|
|
|
|
var loginForm = document.getElementById('login-form');
|
|
var apiKeyToggle = document.getElementById('api-key-toggle');
|
|
var chevron = document.getElementById('api-key-chevron');
|
|
|
|
var ssoBtn = document.getElementById('sso-btn');
|
|
var ssoPollTimer = null;
|
|
|
|
// Reflect live SSO availability: when the OIDC provider is reachable the
|
|
// button is active; when it's down (e.g. provider not yet up after a
|
|
// restart) the button is disabled with a notice, and we poll until the
|
|
// provider recovers — no page reload or click needed.
|
|
function applySsoAvailability(available) {
|
|
if (available) {
|
|
ssoBtn.disabled = false;
|
|
ssoBtn.textContent = 'Sign in with SSO';
|
|
// Clear only our own notice; keep a login error from the callback.
|
|
if (ssoErrorEl.textContent === ssoMessages.unavailable) ssoErrorEl.textContent = '';
|
|
if (ssoPollTimer) { clearInterval(ssoPollTimer); ssoPollTimer = null; }
|
|
} else {
|
|
ssoBtn.disabled = true;
|
|
ssoBtn.textContent = 'SSO unavailable';
|
|
ssoErrorEl.textContent = ssoMessages.unavailable;
|
|
if (!ssoPollTimer) {
|
|
ssoPollTimer = setInterval(function() {
|
|
fetch('/api/auth/status')
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(d) { applySsoAvailability(!!d.oidc_available); })
|
|
.catch(function() {});
|
|
}, 5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if OIDC is enabled
|
|
fetch('/api/auth/status')
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
if (data.oidc_enabled) {
|
|
document.getElementById('sso-section').style.display = 'block';
|
|
apiKeyToggle.style.display = 'block';
|
|
// Hide API key form by default when SSO is available
|
|
loginForm.style.display = 'none';
|
|
applySsoAvailability(!!data.oidc_available);
|
|
}
|
|
if (data.site_title) {
|
|
document.title = data.site_title;
|
|
document.querySelector('h1').textContent = data.site_title;
|
|
}
|
|
initTheme(data.theme);
|
|
});
|
|
|
|
ssoBtn.addEventListener('click', function() {
|
|
if (ssoBtn.disabled) return;
|
|
window.location.href = '/auth/login';
|
|
});
|
|
|
|
// Toggle API key form
|
|
apiKeyToggle.addEventListener('click', function() {
|
|
var hidden = loginForm.style.display === 'none';
|
|
loginForm.style.display = hidden ? '' : 'none';
|
|
chevron.className = hidden ? 'chevron open' : 'chevron';
|
|
});
|
|
|
|
var errorEl = document.getElementById('error');
|
|
var btn = document.getElementById('login-btn');
|
|
|
|
loginForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
errorEl.textContent = '';
|
|
btn.disabled = true;
|
|
btn.textContent = 'Checking...';
|
|
|
|
var key = document.getElementById('api-key').value;
|
|
fetch('/api/sessions', {
|
|
headers: { 'Authorization': 'Bearer ' + key }
|
|
})
|
|
.then(function(res) {
|
|
if (!res.ok) throw new Error('Invalid API key');
|
|
sessionStorage.setItem('rustguac_api_key', key);
|
|
window.location.href = '/connections.html';
|
|
})
|
|
.catch(function(err) {
|
|
errorEl.textContent = err.message;
|
|
})
|
|
.finally(function() {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Login';
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|