feat: render one login button per configured auth provider

This commit is contained in:
2026-08-03 10:56:03 +01:00
parent dde47de145
commit 3a626922a5
3 changed files with 228 additions and 48 deletions
+95 -48
View File
@@ -2,15 +2,42 @@
import { useEffect, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { auth } from "@/lib/api";
import { auth, type PublicProvider } from "@/lib/api";
import { Button, Card } from "@/components/ui";
import { Logo } from "@/components/Logo";
import { NetworkBackground } from "@/components/NetworkBackground";
import { ProviderIcon } from "@/components/settings/ProviderIcon";
const ERROR_MESSAGES: Record<string, string> = {
oidc_unavailable: "Single sign-on is not available on this instance's plan.",
provider_unavailable: "That sign-in method is no longer available.",
provider_unreachable: "Could not reach the identity provider.",
invalid_state: "That sign-in attempt expired. Please try again.",
exchange_failed: "The identity provider rejected the sign-in.",
verification_failed: "The identity provider's response could not be verified.",
missing_id_token: "The identity provider returned no identity token.",
missing_email: "The identity provider returned no email address.",
identity_failed: "Could not read a verified email address from that account.",
provisioning_failed: "Could not create your account on this instance.",
session_failed: "Could not start your session. Please try again.",
state_failed: "Could not start sign-in. Please try again.",
unknown_host: "This address does not name a known instance.",
};
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [instanceName, setInstanceName] = useState("");
const [providers, setProviders] = useState<PublicProvider[]>([]);
// Default true so a slow or failing discovery call still renders something
// usable rather than an empty card.
const [localEnabled, setLocalEnabled] = useState(true);
const [ssoError, setSsoError] = useState("");
useEffect(() => {
const code = new URLSearchParams(window.location.search).get("error");
if (code) setSsoError(ERROR_MESSAGES[code] ?? "Sign-in failed. Please try again.");
}, []);
useEffect(() => {
(async () => {
@@ -22,6 +49,11 @@ export default function LoginPage() {
}
if (s.instance_name) setInstanceName(s.instance_name);
} catch {}
try {
const p = await auth.providers();
setProviders(p.providers);
setLocalEnabled(p.local_enabled);
} catch {}
try {
await auth.me();
window.location.href = "/";
@@ -45,6 +77,9 @@ export default function LoginPage() {
signIn();
}
const showLocal = localEnabled || providers.length === 0;
const showDivider = showLocal && providers.length > 0;
return (
<div className="relative flex min-h-screen items-center justify-center p-4">
<NetworkBackground />
@@ -57,56 +92,68 @@ export default function LoginPage() {
</div>
<Card>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-text-secondary">
Email
</label>
<input
id="email"
type="email"
required
autoComplete="username"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
/>
{ssoError && <div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{ssoError}</div>}
{showLocal && (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-text-secondary">
Email
</label>
<input
id="email"
type="email"
required
autoComplete="username"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
/>
</div>
<div>
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-text-secondary">
Password
</label>
<input
id="password"
type="password"
required
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
/>
</div>
{error && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{(error as Error).message}</div>}
<Button type="submit" variant="primary" loading={isPending} className="w-full justify-center">
Sign In
</Button>
</form>
)}
{showDivider && (
<div className="my-5 flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs uppercase tracking-wider text-text-tertiary">or</span>
<div className="h-px flex-1 bg-border" />
</div>
)}
<div>
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-text-secondary">
Password
</label>
<input
id="password"
type="password"
required
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
/>
{providers.length > 0 && (
<div className="space-y-2">
{providers.map((p) => (
<a key={p.id} href={auth.ssoStartUrl(p.id)} className="block">
<Button type="button" variant="secondary" className="w-full justify-center gap-2">
<ProviderIcon preset={p.preset} />
Continue with {p.name}
</Button>
</a>
))}
</div>
{error && <div className="rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{(error as Error).message}</div>}
<Button type="submit" variant="primary" loading={isPending} className="w-full justify-center">
Sign In
</Button>
</form>
<div className="my-5 flex items-center gap-3">
<div className="h-px flex-1 bg-border" />
<span className="text-xs uppercase tracking-wider text-text-tertiary">or</span>
<div className="h-px flex-1 bg-border" />
</div>
<a href="/auth/oidc/start" className="block">
<Button type="button" variant="secondary" className="w-full justify-center">
Sign in with your instance&apos;s SSO
</Button>
</a>
<p className="mt-3 text-center text-xs text-text-tertiary">SSO must be enabled for this instance by an administrator.</p>
)}
</Card>
</div>
</div>