15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation | 02 | execute | 2 |
|
|
true |
|
|
Purpose: These pages are the visible proof that Phase 1 works. The middleware from Plan 01 protects routes — these pages are what the agent actually sees. Output: /agent/login (branded form) and /agent/dashboard (blank, protected, with logout).
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md @.planning/phases/01-foundation/01-01-SUMMARY.mdFrom src/lib/auth.ts (created in Plan 01):
export const { handlers, auth, signIn, signOut } = NextAuth({ ... });
// auth() — call in server components to get session
// signIn("credentials", { email, password, redirectTo }) — call in server actions
// signOut({ redirectTo }) — call in server actions
From src/lib/db/schema.ts (created in Plan 01):
export const users = pgTable("users", { ... });
// Not needed directly in UI — session comes from auth()
Key behaviors locked by user decisions (CONTEXT.md):
- Login route: /agent/login
- Post-login redirect: /agent/dashboard
- Error message: "Invalid email or password" (generic — do not vary by field)
- Password visibility toggle: included
- Session behavior: 7-day rolling (handled by auth.ts — no code needed here)
- Logout: signOut({ redirectTo: "/agent/login?signed_out=1" })
- Logout confirmation: "You've been signed out" (shown when ?signed_out=1 is in URL)
- No forgot-password link (per deferred decisions — do NOT add it)
Brand assets:
- Agent photo: /Users/ccopeland/Downloads/red.jpg — copy to public/red.jpg
- Brand colors/logo: Use discretion — create CSS custom properties for easy swap later
- Suggested color palette: warm gold (#C9A84C) for accent, deep navy (#1B2B4B) for primary, off-white (#FAF9F7) for background
- Login page should feel like a premium real estate brand, not a generic SaaS admin
Create src/app/agent/login/page.tsx as a server component (no "use client") that:
- Reads
searchParamsto detect?error=invalidand?signed_out=1 - Contains an inline
loginActionserver action that calls signIn - Contains an inline
SignedOutMessageandErrorMessagebased on searchParams - Renders a full-page branded layout with a left/right split or centered card
The login page structure:
- Page fills the full viewport height (min-h-screen)
- Left side (hidden on mobile): Teressa's photo (
/red.jpg) as a cover image, overlaid with a subtle dark gradient and brand name text - Right side (or centered on mobile): Login form card
- "Agent Portal" heading (h1, per user decision)
- If
?signed_out=1: green/teal banner — "You've been signed out." - If
?error=invalid: red banner — "Invalid email or password." - Email input: type="email", name="email", required, label="Email address"
- Password input: type="password" by default, name="password", required, label="Password"
- Password toggle button (show/hide) — this is a client interaction, so wrap just the password field and toggle button in a small "use client" sub-component called
PasswordField - Submit button: "Sign in" text, full-width, shows loading state during submission
- Brand accent color on the submit button and focus rings
The loginAction server action (inside the page file):
async function loginAction(formData: FormData) {
"use server";
try {
await signIn("credentials", {
email: formData.get("email") as string,
password: formData.get("password") as string,
redirectTo: "/agent/dashboard",
});
} catch (error) {
// CRITICAL: Only catch AuthError. Everything else (including NEXT_REDIRECT) must re-throw.
if (error instanceof AuthError) {
redirect("/agent/login?error=invalid");
}
throw error; // Re-throws NEXT_REDIRECT — allows the redirect to fire
}
}
The PasswordField sub-component (can live in same file or separate file, use client):
- Manages
showPasswordboolean state - Renders
<input type={showPassword ? "text" : "password"} ... /> - Renders a toggle button (eye/eye-off icon or text "Show"/"Hide")
- Use Tailwind for all styling — no additional UI libraries
Brand color guide (CSS custom properties or Tailwind classes):
- Primary background: #FAF9F7 (warm off-white)
- Primary text: #1B2B4B (deep navy)
- Accent (buttons, focus rings): #C9A84C (warm gold)
- Error background: #FEF2F2, error text: #991B1B
- Success background: #F0FDF4, success text: #166534
Do NOT add a "Forgot password?" link — this is explicitly deferred per user decisions. Navigate to /agent/login in browser (or run: curl -s http://localhost:3000/agent/login | grep -i "Agent Portal"). Verify:
- Page renders without errors
- Submitting wrong credentials reloads with "Invalid email or password" banner
- Password toggle switches input type between password/text
- npm run build passes with no TypeScript errors in this file Login page renders at /agent/login with branded design, password toggle works, error displays on failed login, signed-out message displays when ?signed_out=1, no TypeScript errors
async function logoutAction() { "use server"; await signOut({ redirectTo: "/agent/login?signed_out=1" }); }
export function LogoutButton() { return (
Sign out ); }
Note: signOut() server action must NOT be caught — it throws NEXT_REDIRECT which must bubble up. The form action pattern avoids the issue.
Create `src/app/agent/layout.tsx` — the shared agent portal layout:
```typescript
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { LogoutButton } from "@/components/ui/LogoutButton";
export default async function AgentLayout({
children,
}: {
children: React.ReactNode;
}) {
// Defense-in-depth: middleware handles most cases, this catches edge cases
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Agent Portal</span>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">{session.user?.email}</span>
<LogoutButton />
</div>
</header>
<main className="p-6">{children}</main>
</div>
);
}
Create src/app/agent/dashboard/page.tsx — blank dashboard stub:
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
// Defense-in-depth session check (layout also checks, this is belt-and-suspenders)
const session = await auth();
if (!session) redirect("/agent/login");
return (
<div>
<h1 className="text-2xl font-semibold text-gray-900">Dashboard</h1>
<p className="mt-2 text-gray-500">
Welcome back, {session.user?.email}. Portal content coming in Phase 3.
</p>
</div>
);
}
Update src/app/page.tsx (public homepage — placeholder for Phase 2):
export default function HomePage() {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">Teressa Copeland Homes</h1>
<p className="mt-4 text-gray-600">Real estate expertise for Utah home buyers and sellers.</p>
<p className="mt-2 text-sm text-gray-400">Marketing site coming in Phase 2.</p>
</div>
</main>
);
}
AUTH-04 verification: The LogoutButton must be accessible from the dashboard (it is in the agent layout header). Clicking it calls logoutAction which calls signOut() with ?signed_out=1, which redirects to /agent/login, which reads the query param and shows "You've been signed out."
AUTH-02 verification: The 7-day rolling JWT session is handled entirely in auth.ts (Plan 01). No additional code needed here — the httpOnly encrypted cookie persists across browser restarts by default. Do not add any sessionStorage usage.
AUTH-03 verification: The middleware from Plan 01 handles unauthenticated redirects. The layout's auth() check is defense-in-depth — not the primary enforcement mechanism. Both layers must be in place.
- Run npm run build — no TypeScript errors
- Verify agent layout exists: ls src/app/agent/layout.tsx
- Verify dashboard exists: ls src/app/agent/dashboard/page.tsx
- Verify LogoutButton exists: ls src/components/ui/LogoutButton.tsx
- TypeScript check: npx tsc --noEmit AgentLayout renders portal header with agent email and sign-out button; DashboardPage shows welcome message with agent email; LogoutButton calls signOut({ redirectTo: "/agent/login?signed_out=1" }); npm run build and npx tsc --noEmit pass with no errors
npm run dev
Manual verification checklist:
- Visit http://localhost:3000/agent/dashboard — should redirect to /agent/login (AUTH-03)
- Submit wrong credentials on /agent/login — should show "Invalid email or password" (AUTH-01)
- Submit correct credentials — should redirect to /agent/dashboard with agent email visible (AUTH-01)
- Refresh /agent/dashboard — should stay logged in, not redirect (AUTH-02 partial — session persists across refresh)
- Click "Sign out" — should redirect to /agent/login with "You've been signed out" message (AUTH-04)
- Visit /agent/dashboard while logged out — should redirect to /agent/login (AUTH-03)
Build verification:
npm run build && echo "BUILD OK"
npx tsc --noEmit && echo "TYPECHECK OK"
<success_criteria>
- /agent/login renders with branded design — photo, "Agent Portal" heading, email/password form
- Password show/hide toggle works
- Invalid login shows "Invalid email or password" (not a more specific error)
- Valid login redirects to /agent/dashboard
- /agent/dashboard shows agent email and Sign out button
- Signing out redirects to /agent/login with "You've been signed out" message
- /agent/dashboard (and all future /agent/* routes) redirect to /agent/login when unauthenticated
- npm run build passes with no errors
- No forgot-password link exists on any page (deferred per user decisions) </success_criteria>
Include in the summary:
- Confirmation that the agent photo was copied from /Users/ccopeland/Downloads/red.jpg (or note if placeholder used)
- Confirmation of the brand colors used (or CSS custom properties defined)
- Manual verification results for the full auth flow (items 1-6 above)
- Any deviations from planned patterns and why