Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ function PortalRoutes() {
}

export default function App() {
const { user, loading, authError, signInWithGoogle } = useAuth();
const { user, loading, authError, signInWithGoogle, signInWithEmail } = useAuth();

// Trigger AI insight generation on contractor login
useEffect(() => {
Expand Down Expand Up @@ -596,7 +596,11 @@ export default function App() {
) : isPortalUser ? (
<Navigate to="/portal" replace />
) : (
<Login onSignIn={signInWithGoogle} error={authError} />
<Login
onSignIn={signInWithGoogle}
onDevSignIn={signInWithEmail}
error={authError}
/>
)
}
/>
Expand Down
42 changes: 38 additions & 4 deletions web/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
onAuthStateChanged,
signInWithPopup,
signInWithRedirect,
signInWithEmailAndPassword,
signOut,
GoogleAuthProvider,
signInWithCustomToken,
Expand All @@ -12,10 +13,16 @@ import { auth } from '../lib/firebase';

const googleProvider = new GoogleAuthProvider();

// Returns true if the user authenticated via Google Sign-In (contractor).
// Custom-token users (portal clients) have providerData from a custom provider.
// Demo/dev allowlist — must match the domain in firestore.rules isContractor().
const DEV_EMAIL_DOMAIN = '@ten99.local';

// Returns true if the user authenticated via Google Sign-In (contractor) or
// via password with an allowlisted dev email. Custom-token users (portal
// clients) have providerData from a custom provider.
export function isContractorUser(user: User): boolean {
return user.providerData.some((p) => p.providerId === 'google.com');
if (user.providerData.some((p) => p.providerId === 'google.com')) return true;
const hasPassword = user.providerData.some((p) => p.providerId === 'password');
return hasPassword && (user.email ?? '').endsWith(DEV_EMAIL_DOMAIN);
}

export function useAuth() {
Expand Down Expand Up @@ -78,6 +85,24 @@ export function useAuth() {
}
};

const signInWithEmail = async (email: string, password: string) => {
setAuthError(null);
if (!email.endsWith(DEV_EMAIL_DOMAIN)) {
const msg = `Dev sign-in is only available for ${DEV_EMAIL_DOMAIN} accounts.`;
setAuthError(msg);
throw new Error(msg);
}
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : 'Email sign-in failed.';
console.error('Email sign-in error:', err);
setAuthError(message);
throw err;
}
};

const signInWithToken = async (token: string) => {
setAuthError(null);
try {
Expand All @@ -97,5 +122,14 @@ export function useAuth() {
setUser(null);
};

return { user, claims, loading, authError, signInWithGoogle, signInWithToken, logout };
return {
user,
claims,
loading,
authError,
signInWithGoogle,
signInWithEmail,
signInWithToken,
logout,
};
}
72 changes: 71 additions & 1 deletion web/src/routes/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { BrandWordmark } from '../components/Brand';

interface LoginProps {
onSignIn: () => Promise<void> | void;
onDevSignIn?: (email: string, password: string) => Promise<void> | void;
error?: string | null;
}

export default function Login({ onSignIn, error }: LoginProps) {
export default function Login({ onSignIn, onDevSignIn, error }: LoginProps) {
const [loading, setLoading] = useState(false);
const [showDev, setShowDev] = useState(false);
const [email, setEmail] = useState('demo@ten99.local');
const [password, setPassword] = useState('');

async function handleSignIn() {
setLoading(true);
Expand All @@ -20,6 +24,19 @@ export default function Login({ onSignIn, error }: LoginProps) {
}
}

async function handleDevSignIn(e: React.FormEvent) {
e.preventDefault();
if (!onDevSignIn) return;
setLoading(true);
try {
await onDevSignIn(email, password);
} catch {
// Error display handled via the error prop
} finally {
setLoading(false);
}
}

return (
<div className="min-h-screen bg-[#2C2417] flex items-center justify-center p-4">
<div className="w-full max-w-sm animate-fade-in-up">
Expand Down Expand Up @@ -68,6 +85,59 @@ export default function Login({ onSignIn, error }: LoginProps) {
</svg>
{loading ? 'Signing in...' : 'Sign in with Google'}
</button>

{onDevSignIn && (
<>
<button
type="button"
onClick={() => setShowDev((v) => !v)}
className="mt-4 w-full text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
>
{showDev ? 'Hide dev sign-in' : 'Use dev sign-in (demo accounts)'}
</button>

{showDev && (
<form onSubmit={handleDevSignIn} className="mt-4 space-y-3">
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1.5">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2.5 rounded-lg bg-[var(--bg-input)] border border-[var(--border)] text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
autoComplete="email"
/>
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-secondary)] mb-1.5">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-3 py-2.5 rounded-lg bg-[var(--bg-input)] border border-[var(--border)] text-sm text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
autoComplete="current-password"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 px-4 rounded-lg bg-[var(--accent)] text-[var(--text-on-accent)] text-sm font-semibold hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
<p className="text-[10px] text-[var(--text-muted)] text-center">
Only @ten99.local demo emails are allowed for dev sign-in.
</p>
</form>
)}
</>
)}
</div>
</div>

Expand Down
Loading