Files
cameleer-server/ui/src/auth/LoginPage.tsx
hsiegeln 50bb22d6f6
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 51s
CI / deploy (push) Successful in 29s
Add OIDC logout, fix OpenAPI schema types, expose end_session_endpoint
Backend:
- Expose end_session_endpoint from OIDC provider metadata in /auth/oidc/config
- Add getEndSessionEndpoint() to OidcTokenExchanger

Frontend:
- On OIDC logout, redirect to provider's end_session_endpoint to clear SSO session
- Strip /api/v1 prefix from OpenAPI paths to match client baseUrl convention
- Add schema-types.ts with convenience type re-exports from generated schema
- Fix all type imports to use schema-types instead of raw generated schema
- Fix optional field access (processors, children, duration) with proper typing
- Fix AgentInstance.state → status field name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:43:18 +01:00

113 lines
3.6 KiB
TypeScript

import { type FormEvent, useEffect, useState } from 'react';
import { Navigate } from 'react-router';
import { useAuthStore } from './auth-store';
import { config } from '../config';
import styles from './LoginPage.module.css';
interface OidcInfo {
clientId: string;
authorizationEndpoint: string;
}
export function LoginPage() {
const { isAuthenticated, login, loading, error } = useAuthStore();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [oidc, setOidc] = useState<OidcInfo | null>(null);
const [oidcLoading, setOidcLoading] = useState(false);
useEffect(() => {
fetch(`${config.apiBaseUrl}/auth/oidc/config`)
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data?.authorizationEndpoint && data?.clientId) {
setOidc({ clientId: data.clientId, authorizationEndpoint: data.authorizationEndpoint });
if (data.endSessionEndpoint) {
localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint);
}
}
})
.catch(() => {});
}, []);
if (isAuthenticated) return <Navigate to="/" replace />;
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
login(username, password);
};
const handleOidcLogin = () => {
if (!oidc) return;
setOidcLoading(true);
const redirectUri = `${window.location.origin}/oidc/callback`;
const params = new URLSearchParams({
response_type: 'code',
client_id: oidc.clientId,
redirect_uri: redirectUri,
scope: 'openid email profile',
});
window.location.href = `${oidc.authorizationEndpoint}?${params}`;
};
return (
<div className={styles.page}>
<form className={styles.card} onSubmit={handleSubmit}>
<div className={styles.logo}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2" />
<path d="M12 6v6l4 2" />
</svg>
cameleer3
</div>
<div className={styles.subtitle}>Sign in to access the observability dashboard</div>
{oidc && (
<>
<button
className={styles.ssoButton}
type="button"
onClick={handleOidcLogin}
disabled={oidcLoading}
>
{oidcLoading ? 'Redirecting...' : 'Sign in with SSO'}
</button>
<div className={styles.divider}>
<span className={styles.dividerText}>or</span>
</div>
</>
)}
<div className={styles.field}>
<label className={styles.label}>Username</label>
<input
className={styles.input}
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
autoComplete="username"
/>
</div>
<div className={styles.field}>
<label className={styles.label}>Password</label>
<input
className={styles.input}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
/>
</div>
<button className={styles.submit} type="submit" disabled={loading || !username || !password}>
{loading ? 'Signing in...' : 'Sign In'}
</button>
{error && <div className={styles.error}>{error}</div>}
</form>
</div>
);
}