Build image / build-and-push (push) Successful in 4s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import { useState, type FormEvent } from 'react'
|
|
import { Link, useNavigate, useSearch } from '@tanstack/react-router'
|
|
import { AuthCard } from '@/components/auth/AuthCard'
|
|
import { Alert } from '@/components/ui/Alert'
|
|
import { Button, buttonClasses } from '@/components/ui/Button'
|
|
import { Divider } from '@/components/ui/Divider'
|
|
import { TextField } from '@/components/ui/TextField'
|
|
import { API_BASE, errorMessage } from '@/lib/api'
|
|
import { useLogin, useProviders } from '@/lib/auth'
|
|
import { safeRedirectPath } from '@/lib/redirect'
|
|
import { usePageTitle } from '@/lib/usePageTitle'
|
|
|
|
// Server-initiated redirect (not a fetch): the browser navigates to the Go
|
|
// handler, which 302s to the IdP.
|
|
const OIDC_LOGIN_URL = `${API_BASE}/auth/oidc/login`
|
|
|
|
// Messages for the ?error= codes the OIDC callback redirects back with.
|
|
const callbackErrors: Record<string, string> = {
|
|
oidc: 'Single sign-on did not complete. Please try again.',
|
|
oidc_unavailable: 'The single sign-on provider is currently unavailable.',
|
|
state: 'Your sign-in attempt expired. Please start again.',
|
|
no_email: 'Your identity provider did not share an email address, which pansy needs.',
|
|
email_unverified: 'Your email address is not verified with the identity provider.',
|
|
oidc_conflict: 'That single sign-on identity conflicts with an existing account.',
|
|
}
|
|
|
|
export function LoginPage() {
|
|
usePageTitle('Sign in')
|
|
const search = useSearch({ from: '/login' })
|
|
const navigate = useNavigate()
|
|
const providers = useProviders()
|
|
const login = useLogin()
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
|
|
const dest = safeRedirectPath(search.redirect)
|
|
const callbackError = search.error ? (callbackErrors[search.error] ?? callbackErrors.oidc) : null
|
|
|
|
async function onSubmit(e: FormEvent) {
|
|
e.preventDefault()
|
|
try {
|
|
await login.mutateAsync({ email, password })
|
|
await navigate({ to: dest })
|
|
} catch {
|
|
// Shown below via login.error.
|
|
}
|
|
}
|
|
|
|
const local = providers.data?.local ?? false
|
|
const oidc = providers.data?.oidc ?? false
|
|
|
|
return (
|
|
<AuthCard title="Sign in to pansy" subtitle="Plan your garden, bed by bed.">
|
|
<div className="flex flex-col gap-4">
|
|
{callbackError && <Alert>{callbackError}</Alert>}
|
|
|
|
{providers.isPending && <p className="text-sm text-muted">Loading…</p>}
|
|
{providers.isError && <Alert>Could not load sign-in options. Please refresh.</Alert>}
|
|
|
|
{oidc && (
|
|
<a href={OIDC_LOGIN_URL} className={buttonClasses('primary', 'w-full')}>
|
|
{providers.data?.oidcLabel || 'Sign in with SSO'}
|
|
</a>
|
|
)}
|
|
|
|
{oidc && local && <Divider>or</Divider>}
|
|
|
|
{local && (
|
|
<form onSubmit={onSubmit} className="flex flex-col gap-3">
|
|
<TextField
|
|
label="Email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
autoCapitalize="none"
|
|
spellCheck={false}
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
<TextField
|
|
label="Password"
|
|
name="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
{login.isError && <Alert>{errorMessage(login.error, 'Could not sign in.')}</Alert>}
|
|
<Button type="submit" disabled={login.isPending} className="mt-1">
|
|
{login.isPending ? 'Signing in…' : 'Sign in'}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
|
|
{providers.isSuccess && !local && !oidc && (
|
|
<Alert tone="info">No sign-in methods are configured on this server.</Alert>
|
|
)}
|
|
|
|
{local && (
|
|
<p className="text-center text-sm text-muted">
|
|
No account?{' '}
|
|
<Link to="/register" className="font-medium text-accent-strong hover:underline">
|
|
Create one
|
|
</Link>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</AuthCard>
|
|
)
|
|
}
|