Files

349 lines
15 KiB
Markdown
Raw Permalink Normal View History

---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on:
- "01-01"
files_modified:
- src/app/agent/login/page.tsx
- src/app/agent/dashboard/page.tsx
- src/app/agent/layout.tsx
- src/components/ui/LogoutButton.tsx
- src/app/layout.tsx
- src/app/page.tsx
- public/red.jpg
autonomous: true
requirements:
- AUTH-01
- AUTH-02
- AUTH-03
- AUTH-04
must_haves:
truths:
- "Visiting /agent/login displays a branded form with email, password (with show/hide toggle), and a submit button"
- "Submitting invalid credentials shows 'Invalid email or password' without hinting which field is wrong"
- "Submitting valid credentials redirects to /agent/dashboard"
- "After login, the agent's email is visible somewhere on the dashboard"
- "The dashboard has a logout button that, when clicked, redirects to /agent/login"
- "Visiting /agent/login after logout shows 'You've been signed out' confirmation message"
- "Visiting /agent/dashboard without a session redirects to /agent/login (enforced by middleware from Plan 01)"
artifacts:
- path: "src/app/agent/login/page.tsx"
provides: "Branded login page with email/password form, password toggle, error display, signed-out confirmation"
exports: ["default (LoginPage)"]
- path: "src/app/agent/dashboard/page.tsx"
provides: "Protected blank dashboard — server component that calls auth() and shows agent email + logout button"
exports: ["default (DashboardPage)"]
- path: "src/app/agent/layout.tsx"
provides: "Agent portal layout wrapper — defense-in-depth session check, shared nav shell"
exports: ["default (AgentLayout)"]
- path: "src/components/ui/LogoutButton.tsx"
provides: "Client component with logout form that calls signOut server action"
exports: ["LogoutButton"]
key_links:
- from: "src/app/agent/login/page.tsx"
to: "src/lib/auth.ts"
via: "loginAction() calls signIn('credentials', ...) from @/lib/auth"
pattern: "signIn.*credentials"
- from: "src/components/ui/LogoutButton.tsx"
to: "src/lib/auth.ts"
via: "logoutAction() calls signOut({ redirectTo: '/agent/login?signed_out=1' })"
pattern: "signOut.*signed_out"
- from: "src/app/agent/dashboard/page.tsx"
to: "src/lib/auth.ts"
via: "const session = await auth() — defense-in-depth beyond middleware"
pattern: "await auth\\(\\)"
---
<objective>
Build the two agent-facing pages (login and dashboard) and the logout mechanism. The login page is fully branded with Teressa's photo and implements all user-specified behaviors. The dashboard is a blank stub that confirms the session is valid and provides a logout button. Together these pages satisfy all four AUTH requirements.
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).
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<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.md
<interfaces>
<!-- Contracts from Plan 01 that this plan consumes. -->
<!-- Do not import from other paths — these are the established contracts. -->
From src/lib/auth.ts (created in Plan 01):
```typescript
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):
```typescript
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
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Build the branded login page with password toggle and error handling</name>
<files>src/app/agent/login/page.tsx, public/red.jpg</files>
<action>
Copy the agent photo from its source path to the public directory:
```bash
cp /Users/ccopeland/Downloads/red.jpg /path/to/project/public/red.jpg
```
If the file does not exist at that path, use a placeholder image (create a simple Next.js Image with a colored background placeholder) and note it in the SUMMARY.md.
Create `src/app/agent/login/page.tsx` as a **server component** (no "use client") that:
1. Reads `searchParams` to detect `?error=invalid` and `?signed_out=1`
2. Contains an inline `loginAction` server action that calls signIn
3. Contains an inline `SignedOutMessage` and `ErrorMessage` based on searchParams
4. 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):
```typescript
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 `showPassword` boolean 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.
</action>
<verify>
Navigate to /agent/login in browser (or run: curl -s http://localhost:3000/agent/login | grep -i "Agent Portal"). Verify:
1. Page renders without errors
2. Submitting wrong credentials reloads with "Invalid email or password" banner
3. Password toggle switches input type between password/text
4. npm run build passes with no TypeScript errors in this file
</verify>
<done>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</done>
</task>
<task type="auto">
<name>Task 2: Build the agent portal layout, dashboard stub, and logout mechanism</name>
<files>src/app/agent/layout.tsx, src/app/agent/dashboard/page.tsx, src/components/ui/LogoutButton.tsx, src/app/page.tsx</files>
<action>
Create `src/components/ui/LogoutButton.tsx` as a client component with a server action for logout:
```typescript
// src/components/ui/LogoutButton.tsx
import { signOut } from "@/lib/auth";
async function logoutAction() {
"use server";
await signOut({ redirectTo: "/agent/login?signed_out=1" });
}
export function LogoutButton() {
return (
<form action={logoutAction}>
<button
type="submit"
className="text-sm text-gray-600 hover:text-gray-900 underline"
>
Sign out
</button>
</form>
);
}
```
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:
```typescript
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):
```typescript
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.
</action>
<verify>
1. Run npm run build — no TypeScript errors
2. Verify agent layout exists: ls src/app/agent/layout.tsx
3. Verify dashboard exists: ls src/app/agent/dashboard/page.tsx
4. Verify LogoutButton exists: ls src/components/ui/LogoutButton.tsx
5. TypeScript check: npx tsc --noEmit
</verify>
<done>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</done>
</task>
</tasks>
<verification>
Full auth flow verification (requires running dev server):
```bash
npm run dev
```
Manual verification checklist:
1. Visit http://localhost:3000/agent/dashboard — should redirect to /agent/login (AUTH-03)
2. Submit wrong credentials on /agent/login — should show "Invalid email or password" (AUTH-01)
3. Submit correct credentials — should redirect to /agent/dashboard with agent email visible (AUTH-01)
4. Refresh /agent/dashboard — should stay logged in, not redirect (AUTH-02 partial — session persists across refresh)
5. Click "Sign out" — should redirect to /agent/login with "You've been signed out" message (AUTH-04)
6. Visit /agent/dashboard while logged out — should redirect to /agent/login (AUTH-03)
Build verification:
```bash
npm run build && echo "BUILD OK"
npx tsc --noEmit && echo "TYPECHECK OK"
```
</verification>
<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>
<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` using the summary template.
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
</output>