Added sso

This commit is contained in:
bobban committed 2026-09-04 23:12:40 +02:00
1 parent c1c40361a7
commit 1aa64c93ae
5 files changed
+278 -3

No files matched your search

+42
View File
@@ -731,6 +731,48 @@ body {
.login-btn { width: 100%; padding: 10px; font-size: 14px; margin-top: 4px; }
/* SSO button */
.btn-sso {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 10px 14px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-sso:hover {
background: var(--hover-bg, rgba(128,128,128,0.08));
border-color: var(--accent);
}
.btn-sso:before {
content: '🔑';
font-size: 16px;
}
.login-divider {
display: flex;
align-items: center;
gap: 10px;
margin: 14px 0 10px;
color: var(--text-muted);
font-size: 12px;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.login-footer {
margin-top: 24px;
font-size: 12px;
+16
View File
@@ -54,6 +54,22 @@ export async function changePassword(currentPassword, newPassword) {
}));
}
// ─── OIDC / SSO ──────────────────────────────────────────────────────────────
export async function getOidcConfig() {
const res = await fetch(`${BASE}/auth/oidc/config`);
return handleResponse(res);
}
export async function oidcCallback(code, redirectUri) {
const res = await fetch(`${BASE}/auth/oidc/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, redirectUri }),
});
return handleResponse(res);
}
// ─── Users ────────────────────────────────────────────────────────────────────
export async function getUsers() {
+101 -3
View File
@@ -1,5 +1,12 @@
import { useState } from 'react';
import { login, setToken } from '../api/dns';
import { useState, useEffect } from 'react';
import { login, setToken, getOidcConfig, oidcCallback } from '../api/dns';
const OIDC_STATE_KEY = 'sloth_oidc_state';
function getRedirectUri() {
// Strip any existing query/hash — the redirect_uri must be the clean app URL
return `${window.location.origin}${window.location.pathname}`;
}
export default function LoginPage({ onLogin }) {
const [username, setUsername] = useState('');
@@ -7,6 +14,69 @@ export default function LoginPage({ onLogin }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// OIDC state
const [oidc, setOidc] = useState(null); // null = loading, false = disabled
const [oidcLoading, setOidcLoading] = useState(false);
// 1. Fetch OIDC config on mount
useEffect(() => {
getOidcConfig()
.then(cfg => setOidc(cfg.enabled ? cfg : false))
.catch(() => setOidc(false));
}, []);
// 2. Handle redirect back from Authentik (?code=...&state=...)
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (!code) return;
// Verify state to prevent CSRF
const savedState = sessionStorage.getItem(OIDC_STATE_KEY);
sessionStorage.removeItem(OIDC_STATE_KEY);
if (state && savedState && state !== savedState) {
setError('SSO state mismatch — please try again');
// Remove code from URL
window.history.replaceState({}, '', window.location.pathname);
return;
}
// Exchange code for JWT
setOidcLoading(true);
setError('');
const redirectUri = getRedirectUri();
// Clean URL before async work so a page refresh won't re-attempt
window.history.replaceState({}, '', window.location.pathname);
oidcCallback(code, redirectUri)
.then(data => {
setToken(data.token);
onLogin(data.user);
})
.catch(err => {
setError(err.message);
setOidcLoading(false);
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// 3. Initiate Authentik redirect
function handleSsoClick() {
if (!oidc) return;
const state = crypto.randomUUID();
sessionStorage.setItem(OIDC_STATE_KEY, state);
const params = new URLSearchParams({
response_type: 'code',
client_id: oidc.clientId,
redirect_uri: getRedirectUri(),
scope: oidc.scope,
state,
});
window.location.href = `${oidc.authorizationEndpoint}?${params}`;
}
// 4. Local login
async function handleSubmit(e) {
e.preventDefault();
setError('');
@@ -22,11 +92,39 @@ export default function LoginPage({ onLogin }) {
}
}
// While exchanging an OIDC code, show a minimal loading screen
if (oidcLoading) {
return (
<div className="login-wrapper">
<div className="login-box">
<div className="login-logo">🦥</div>
<h1 className="login-title">Sloth Manager</h1>
<p style={{ textAlign: 'center', marginTop: '1rem' }}>Signing in with Authentik…</p>
</div>
</div>
);
}
return (
<div className="login-wrapper">
<div className="login-box">
<div className="login-logo">🦥</div>
<h1 className="login-title">Sloth Manager</h1>
{/* SSO button — shown only when Authentik is configured */}
{oidc && (
<>
<button
type="button"
className="btn-sso"
onClick={handleSsoClick}
>
Sign in with Authentik
</button>
<div className="login-divider"><span>or</span></div>
</>
)}
<form onSubmit={handleSubmit} className="login-form">
<label>
Username
@@ -34,7 +132,7 @@ export default function LoginPage({ onLogin }) {
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
autoFocus
autoFocus={!oidc}
autoComplete="username"
required
/>