Files
cameleer-server/ui/src/auth/LoginPage.tsx

127 lines
4.0 KiB
TypeScript
Raw Normal View History

import { type FormEvent, useEffect, useState } from 'react';
import { Navigate } from 'react-router';
import { useAuthStore } from './auth-store';
import { api } from '../api/client';
import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system';
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(() => {
api.GET('/auth/oidc/config')
.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}>
<Card className={styles.card}>
<div className={styles.loginForm}>
<div className={styles.logo}>cameleer3</div>
<p className={styles.subtitle}>Sign in to access the observability dashboard</p>
{error && (
<div className={styles.error}>
<Alert variant="error">{error}</Alert>
</div>
)}
{oidc && (
<>
<div className={styles.socialSection}>
<Button
variant="secondary"
className={styles.ssoButton}
onClick={handleOidcLogin}
disabled={oidcLoading}
type="button"
>
{oidcLoading ? 'Redirecting...' : 'Sign in with SSO'}
</Button>
</div>
<div className={styles.divider}>
<div className={styles.dividerLine} />
<span className={styles.dividerText}>or</span>
<div className={styles.dividerLine} />
</div>
</>
)}
<form className={styles.fields} onSubmit={handleSubmit} aria-label="Sign in" noValidate>
<FormField label="Username" htmlFor="login-username">
<Input
id="login-username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
autoFocus
autoComplete="username"
disabled={loading}
/>
</FormField>
<FormField label="Password" htmlFor="login-password">
<Input
id="login-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
autoComplete="current-password"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || !username || !password}
className={styles.submitButton}
>
Sign in
</Button>
</form>
</div>
</Card>
</div>
);
}