SAAS_PLAN.md 2.2: there was no /signup page. The only self-serve path was Google sign-in on /login, which auto-provisions a Firebase account with no role or org claim at all - previously that meant "viewer role, full read access" the moment the AuthProvider cookie logic (previous commit) let it through. That's closed now regardless; this commit is the other side of it - giving people an actual way in. app/signup/page.tsx: email/password (createUserWithEmailAndPassword) or Google, same visual language as /login. It only creates the Firebase account - org naming is deliberately not on this page, so every path that produces an account with no org (this one, and Google-via-/login) converges on the same next screen. app/onboarding/page.tsx: that screen. Shown to any signed-in user with no orgId (ChromeSwitcher's redirect, previous commit), collects an org name, calls the new c2api.signup() -> POST /auth/signup (routers/links.py, already shipped), then refreshClaims() to force-refetch the ID token so orgId picks up immediately and the same redirect effect sends them on to /dashboard - no manual reload needed. lib/c2api.ts also gained getOrg/updateOrg and the enrollment-token mint/list/revoke calls (routers/org.py, already shipped on the backend) and joinWaitlist (routers/waitlist.py) - none consumed yet, wired in ahead of the settings/legal commits that use them so this stays one add per concept rather than scattering client additions across later commits. /login gained a "Don't have an account? Sign up" link to /signup. This is signup plumbing, not marketing copy - pricing/plan copy (app/pricing, lib/billing.ts) is untouched in this pass, that's a separate, still-open decision (SAAS_PLAN.md section 6). Typecheck: clean (tsc --noEmit via the WSL-native ~/drb-frontend copy). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
4.7 KiB
TypeScript
117 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { signInWithEmailAndPassword, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
|
|
import { auth } from "@/lib/firebase";
|
|
import { c2api } from "@/lib/c2api";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
export default function LoginPage() {
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const router = useRouter();
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await signInWithEmailAndPassword(auth, email, password);
|
|
c2api.recordSession().catch(() => {});
|
|
router.push("/dashboard");
|
|
} catch {
|
|
setError("Invalid email or password.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleGoogle() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await signInWithPopup(auth, new GoogleAuthProvider());
|
|
c2api.recordSession().catch(() => {});
|
|
router.push("/dashboard");
|
|
} catch {
|
|
setError("Google sign-in failed. Try again.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-sm mx-auto pt-16">
|
|
<Link href="/" className="flex items-center justify-center gap-2 mb-6 font-mono font-bold text-white">
|
|
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-indigo-600 text-white">D</span>
|
|
DRB
|
|
</Link>
|
|
<div className="bg-gray-900 border border-gray-700 rounded-xl p-8 space-y-5 font-mono">
|
|
<h1 className="text-white text-lg font-bold">Sign in</h1>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="text-xs text-gray-400 block mb-1">Email</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
autoComplete="email"
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-gray-400 block mb-1">Password</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
autoComplete="current-password"
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
{error && <p className="text-red-400 text-xs">{error}</p>}
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg py-2 text-sm font-semibold transition-colors"
|
|
>
|
|
{loading ? "Signing in…" : "Sign in"}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex-1 h-px bg-gray-700" />
|
|
<span className="text-xs text-gray-500">or</span>
|
|
<div className="flex-1 h-px bg-gray-700" />
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleGoogle}
|
|
disabled={loading}
|
|
className="w-full flex items-center justify-center gap-3 bg-white hover:bg-gray-100 disabled:opacity-50 text-gray-900 rounded-lg py-2 text-sm font-semibold transition-colors"
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" fill="#4285F4"/>
|
|
<path d="M9 18c2.43 0 4.467-.806 5.956-2.184l-2.908-2.258c-.806.54-1.837.859-3.048.859-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z" fill="#34A853"/>
|
|
<path d="M3.964 10.706A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.706V4.962H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.038l3.007-2.332z" fill="#FBBC05"/>
|
|
<path d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.962L3.964 6.294C4.672 4.169 6.656 3.58 9 3.58z" fill="#EA4335"/>
|
|
</svg>
|
|
Continue with Google
|
|
</button>
|
|
|
|
<p className="text-center text-xs text-gray-500">
|
|
Don't have an account?{" "}
|
|
<Link href="/signup" className="text-indigo-400 hover:text-indigo-300 transition-colors">Sign up</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|