/** * Sign-in. * * Two methods, because they suit different situations. A password is faster * for someone who uses PIG daily and has it in a manager; a magic link needs * no credential at all and is the better answer for someone signing in once * from a phone. Neither is stored by PIG — both are handled entirely by the * identity provider, and PIG only ever sees the resulting token. * * Password is the default tab because the alternative — defaulting to a link * and making daily users switch every time — is the more annoying of the two * mistakes. */ import { useState } from 'react'; import { Mail } from 'lucide-react'; import { getSupabase, type PublicConfig } from '@/lib/api'; import { Button, Input, Label } from '@/components/ui'; import { FormField } from '@/components/ui/form-field'; import { AuthShell } from '@/components/AuthShell'; import { usePageTitle } from '@/lib/title'; type Method = 'password' | 'link'; export function SignIn({ config, onCreateAccount, }: { config: PublicConfig; onCreateAccount: () => void; }) { usePageTitle('Sign in'); const [method, setMethod] = useState('password'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [status, setStatus] = useState<'idle' | 'busy' | 'sent' | 'error'>('idle'); const [message, setMessage] = useState(''); async function submit(event: React.FormEvent) { event.preventDefault(); const supabase = getSupabase(); if (!supabase) { setStatus('error'); setMessage('Authentication is not configured on this deployment.'); return; } setStatus('busy'); setMessage(''); if (method === 'password') { const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) { setStatus('error'); // Supabase deliberately returns the same message for a wrong password // and an unknown address, which is correct — distinguishing them tells // an attacker which addresses are registered. setMessage(error.message); return; } // The auth state listener in App.tsx picks this up and re-fetches; no // navigation is needed here. setStatus('idle'); return; } const { error } = await supabase.auth.signInWithOtp({ email, options: { emailRedirectTo: window.location.origin }, }); if (error) { setStatus('error'); setMessage(error.message); return; } setStatus('sent'); } return (
{status === 'sent' ? (

Check your email

A sign-in link is on its way to {email}. It expires shortly, so use it soon.

) : ( <>

Sign in to PIG

Authentication stays with the deployment's identity provider. PIG never stores your password.

{( [ { key: 'password', label: 'Password' }, { key: 'link', label: 'Email link' }, ] as const ).map((option) => ( ))}
setEmail(e.target.value)} placeholder="you@company.com" autoComplete="username" autoCapitalize="off" autoCorrect="off" spellCheck={false} /> {method === 'password' ? ( setPassword(e.target.value)} // `current-password` is what lets a password manager // offer to fill, and iOS to offer a saved credential. autoComplete="current-password" /> ) : null} {status === 'error' ? (

{message}

) : null} {config.canSelfRegister ? (

Have a PIG invite code but no account? Registration remains closed on the shared identity provider.

) : config.inviteRequired ? (

This workspace is invite-only. Ask an administrator to provision access; this screen never opens self-registration.

) : null}
)}
); }