Map fontWeight: 700 → 600 in non-title contexts (LoginPage, FileDropZone). Align FileDropZone badge padding to 4px grid. Closes #70
203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
import { useEffect, useState, type FormEvent } from "react";
|
|
import { useAuth } from "../hooks/useAuth";
|
|
import { get } from "../api/client";
|
|
import type { AuthConfig } from "../api/types";
|
|
|
|
export function LoginPage() {
|
|
const { login } = useAuth();
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [oidcEnabled, setOidcEnabled] = useState(false);
|
|
|
|
useEffect(() => {
|
|
get<AuthConfig>("/api/auth/config")
|
|
.then((cfg) => setOidcEnabled(cfg.oidc_enabled))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
if (!username.trim() || !password) return;
|
|
setError("");
|
|
setSubmitting(true);
|
|
try {
|
|
await login(username.trim(), password);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Login failed");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={containerStyle}>
|
|
<div style={cardStyle}>
|
|
<h1 style={titleStyle}>Silo</h1>
|
|
<p style={subtitleStyle}>Product Lifecycle Management</p>
|
|
|
|
{error && <div style={errorStyle}>{error}</div>}
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div style={formGroupStyle}>
|
|
<label style={labelStyle}>Username</label>
|
|
<input
|
|
className="silo-input"
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="Username or LDAP uid"
|
|
autoFocus
|
|
required
|
|
style={inputStyle}
|
|
/>
|
|
</div>
|
|
<div style={formGroupStyle}>
|
|
<label style={labelStyle}>Password</label>
|
|
<input
|
|
className="silo-input"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Password"
|
|
required
|
|
style={inputStyle}
|
|
/>
|
|
</div>
|
|
<button type="submit" disabled={submitting} style={btnPrimaryStyle}>
|
|
{submitting ? "Signing in..." : "Sign In"}
|
|
</button>
|
|
</form>
|
|
|
|
{oidcEnabled && (
|
|
<>
|
|
<div style={dividerStyle}>
|
|
<span style={dividerLineStyle} />
|
|
<span
|
|
style={{
|
|
padding: "0 1rem",
|
|
color: "var(--ctp-overlay0)",
|
|
fontSize: "var(--font-body)",
|
|
}}
|
|
>
|
|
or
|
|
</span>
|
|
<span style={dividerLineStyle} />
|
|
</div>
|
|
<a href="/auth/oidc" style={btnOidcStyle}>
|
|
Sign in with Keycloak
|
|
</a>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const containerStyle: React.CSSProperties = {
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
minHeight: "100vh",
|
|
backgroundColor: "var(--ctp-base)",
|
|
};
|
|
|
|
const cardStyle: React.CSSProperties = {
|
|
backgroundColor: "var(--ctp-surface0)",
|
|
borderRadius: "1rem",
|
|
padding: "2.5rem",
|
|
width: "100%",
|
|
maxWidth: 400,
|
|
margin: "1rem",
|
|
};
|
|
|
|
const titleStyle: React.CSSProperties = {
|
|
color: "var(--ctp-mauve)",
|
|
textAlign: "center",
|
|
fontSize: "2rem",
|
|
fontWeight: 600,
|
|
marginBottom: "0.25rem",
|
|
};
|
|
|
|
const subtitleStyle: React.CSSProperties = {
|
|
color: "var(--ctp-subtext0)",
|
|
textAlign: "center",
|
|
fontSize: "var(--font-body)",
|
|
marginBottom: "2rem",
|
|
};
|
|
|
|
const errorStyle: React.CSSProperties = {
|
|
color: "var(--ctp-red)",
|
|
background: "rgba(243, 139, 168, 0.1)",
|
|
border: "1px solid rgba(243, 139, 168, 0.2)",
|
|
padding: "0.75rem 1rem",
|
|
borderRadius: "0.5rem",
|
|
marginBottom: "1rem",
|
|
fontSize: "var(--font-body)",
|
|
};
|
|
|
|
const formGroupStyle: React.CSSProperties = {
|
|
marginBottom: "1.25rem",
|
|
};
|
|
|
|
const labelStyle: React.CSSProperties = {
|
|
display: "block",
|
|
marginBottom: "0.5rem",
|
|
fontWeight: 500,
|
|
color: "var(--ctp-subtext1)",
|
|
fontSize: "var(--font-body)",
|
|
};
|
|
|
|
const inputStyle: React.CSSProperties = {
|
|
width: "100%",
|
|
padding: "0.75rem 1rem",
|
|
backgroundColor: "var(--ctp-base)",
|
|
border: "1px solid var(--ctp-surface1)",
|
|
borderRadius: "0.5rem",
|
|
color: "var(--ctp-text)",
|
|
fontSize: "var(--font-body)",
|
|
boxSizing: "border-box",
|
|
};
|
|
|
|
const btnPrimaryStyle: React.CSSProperties = {
|
|
display: "block",
|
|
width: "100%",
|
|
padding: "0.75rem 1.5rem",
|
|
borderRadius: "0.25rem",
|
|
fontWeight: 500,
|
|
fontSize: "0.75rem",
|
|
cursor: "pointer",
|
|
border: "none",
|
|
backgroundColor: "var(--ctp-mauve)",
|
|
color: "var(--ctp-crust)",
|
|
textAlign: "center",
|
|
};
|
|
|
|
const dividerStyle: React.CSSProperties = {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
margin: "1.5rem 0",
|
|
};
|
|
|
|
const dividerLineStyle: React.CSSProperties = {
|
|
flex: 1,
|
|
borderTop: "1px solid var(--ctp-surface1)",
|
|
};
|
|
|
|
const btnOidcStyle: React.CSSProperties = {
|
|
display: "block",
|
|
width: "100%",
|
|
padding: "0.75rem 1.5rem",
|
|
borderRadius: "0.25rem",
|
|
fontWeight: 500,
|
|
fontSize: "0.75rem",
|
|
cursor: "pointer",
|
|
border: "none",
|
|
backgroundColor: "var(--ctp-blue)",
|
|
color: "var(--ctp-crust)",
|
|
textAlign: "center",
|
|
textDecoration: "none",
|
|
boxSizing: "border-box",
|
|
};
|