Changeset 0.24.1 (#157)

This commit is contained in:
2026-03-07 17:11:32 +00:00
parent a63728a481
commit b6cc4df6e7
27 changed files with 2094 additions and 453 deletions

View File

@@ -71,6 +71,7 @@ type PageData struct {
LogoURL string // branding: custom logo URL
Tagline string // branding: tagline under instance name
RegistrationOpen bool // whether self-registration is enabled
AuthMode string // v0.24.1: "builtin", "mtls", "oidc"
}
// UserContext is the authenticated user's info available to templates.
@@ -211,6 +212,7 @@ func (e *Engine) RenderLogin() gin.HandlerFunc {
LogoURL: logoURL,
Tagline: tagline,
RegistrationOpen: regOpen,
AuthMode: e.cfg.AuthMode,
})
}
}

View File

@@ -209,6 +209,28 @@
<p id="authSubtitle">Sign in to your Switchboard instance</p>
</div>
{{if eq .AuthMode "oidc"}}
<!-- SSO LOGIN -->
<div class="anim anim-a2">
<p style="color: var(--text-2); font-size: 0.9rem; margin-bottom: 1.5rem;">
This instance uses Single Sign-On. Click below to authenticate with your identity provider.
</p>
<a class="btn-login" href="{{.BasePath}}/api/v1/auth/oidc/login" style="display:block; text-align:center; text-decoration:none;">
Sign in with SSO
</a>
</div>
{{else if eq .AuthMode "mtls"}}
<!-- mTLS LOGIN -->
<div class="anim anim-a2">
<p style="color: var(--text-2); font-size: 0.9rem; margin-bottom: 1.5rem;">
This instance uses client certificate authentication. Ensure your browser has a valid certificate installed.
</p>
<a class="btn-login" href="{{.BasePath}}/" style="display:block; text-align:center; text-decoration:none;">
Continue with Certificate
</a>
</div>
{{else}}
<!-- BUILTIN LOGIN -->
<div class="auth-tabs anim anim-a2">
<button class="auth-tab active" id="tabLogin" onclick="_switchTab('login')">Log In</button>
{{if .RegistrationOpen}}<button class="auth-tab" id="tabRegister" onclick="_switchTab('register')">Register</button>{{end}}
@@ -251,6 +273,7 @@
<button class="btn-login" id="regBtn" onclick="_doRegister()">Create Account</button>
</div>
{{end}}
{{end}}{{/* end auth mode conditional */}}
<div class="auth-footer">
<p>Self-hosted instance &middot; Your data stays on your infrastructure</p>
@@ -272,29 +295,60 @@
</script>
<script src="{{.BasePath}}/js/pages.js?v={{.Version}}"></script>
<script>
// ── OIDC SSO callback handler ───────────────
// The OIDC callback redirects here with #oidc_result=<base64 JSON>.
// We decode it, save to localStorage (same format as builtin login),
// set the page-auth cookie, and redirect to the app.
(function() {
const hash = window.location.hash;
if (!hash.startsWith('#oidc_result=')) return;
try {
const encoded = hash.slice('#oidc_result='.length);
const json = atob(encoded);
const data = JSON.parse(json);
const base = window.__BASE__ || '';
const storageKey = base ? 'sb_auth_' + base.replace(/\//g, '') : 'sb_auth';
localStorage.setItem(storageKey, JSON.stringify({
accessToken: data.access_token,
refreshToken: data.refresh_token,
user: data.user,
}));
document.cookie = 'sb_token=' + data.access_token + '; path=/; max-age=900; SameSite=Strict';
window.location.replace(base + '/');
} catch (e) {
console.error('[oidc] failed to process SSO callback:', e);
}
})();
</script>
<script>
// Builtin auth handlers — safe to include always; no-op if DOM elements absent.
function _switchTab(tab) {
document.getElementById('loginForm').style.display = tab === 'login' ? '' : 'none';
const lf = document.getElementById('loginForm');
if (lf) lf.style.display = tab === 'login' ? '' : 'none';
const regForm = document.getElementById('registerForm');
if (regForm) regForm.style.display = tab === 'register' ? '' : 'none';
document.getElementById('tabLogin').classList.toggle('active', tab === 'login');
const regTab = document.getElementById('tabRegister');
if (regTab) regTab.classList.toggle('active', tab === 'register');
document.getElementById('authTitle').textContent = tab === 'login' ? 'Welcome back' : 'Create account';
document.getElementById('authSubtitle').textContent = tab === 'login'
document.getElementById('tabLogin')?.classList.toggle('active', tab === 'login');
document.getElementById('tabRegister')?.classList.toggle('active', tab === 'register');
const title = document.getElementById('authTitle');
if (title) title.textContent = tab === 'login' ? 'Welcome back' : 'Create account';
const sub = document.getElementById('authSubtitle');
if (sub) sub.textContent = tab === 'login'
? 'Sign in to your Switchboard instance'
: 'Join your team on Switchboard';
document.getElementById('loginError').textContent = '';
const loginErr = document.getElementById('loginError');
if (loginErr) loginErr.textContent = '';
const regErr = document.getElementById('regError');
if (regErr) { regErr.textContent = ''; regErr.style.display = 'none'; }
}
async function _doRegister() {
const username = document.getElementById('regUsername').value.trim();
const email = document.getElementById('regEmail').value.trim();
const password = document.getElementById('regPassword').value;
const confirm = document.getElementById('regConfirm').value;
const username = document.getElementById('regUsername')?.value.trim();
const email = document.getElementById('regEmail')?.value.trim();
const password = document.getElementById('regPassword')?.value;
const confirm = document.getElementById('regConfirm')?.value;
const errEl = document.getElementById('regError');
const btn = document.getElementById('regBtn');
if (!errEl || !btn) return;
if (!username || !password) { errEl.textContent = 'Username and password are required'; errEl.style.display = ''; return; }
if (password.length < 8) { errEl.textContent = 'Password must be at least 8 characters'; errEl.style.display = ''; return; }
@@ -322,7 +376,6 @@
errEl.style.display = '';
setTimeout(() => { errEl.style.color = ''; errEl.style.display = 'none'; _switchTab('login'); }, 3000);
} else {
// Auto-login
const storageKey = base ? 'sb_auth_' + base.replace(/\//g, '') : 'sb_auth';
localStorage.setItem(storageKey, JSON.stringify({
accessToken: data.access_token, refreshToken: data.refresh_token, user: data.user,
@@ -338,12 +391,11 @@
}
}
// Enter key handlers
document.getElementById('loginPassword').addEventListener('keydown', e => {
document.getElementById('loginPassword')?.addEventListener('keydown', e => {
if (e.key === 'Enter') Pages.doLogin();
});
document.getElementById('loginUsername').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('loginPassword').focus();
document.getElementById('loginUsername')?.addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('loginPassword')?.focus();
});
document.getElementById('regConfirm')?.addEventListener('keydown', e => {
if (e.key === 'Enter') _doRegister();