docs(01-foundation): create phase 1 plan
This commit is contained in:
@@ -32,9 +32,12 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
3. Visiting any `/agent/*` route while unauthenticated redirects to the login page (not a 404 or blank screen)
|
3. Visiting any `/agent/*` route while unauthenticated redirects to the login page (not a 404 or blank screen)
|
||||||
4. Agent can log out from the portal and is returned to the login page
|
4. Agent can log out from the portal and is returned to the login page
|
||||||
5. Database schema is deployed to Neon, Vercel Blob bucket is created, and the project is deployed to Vercel with environment variables wired
|
5. Database schema is deployed to Neon, Vercel Blob bucket is created, and the project is deployed to Vercel with environment variables wired
|
||||||
**Plans**: TBD
|
**Plans**: 3 plans
|
||||||
|
|
||||||
Plans: none yet
|
Plans:
|
||||||
|
- [ ] 01-01-PLAN.md — Next.js scaffold, Drizzle schema, Auth.js v5 config, route-protection middleware, seed script
|
||||||
|
- [ ] 01-02-PLAN.md — Branded login page, agent portal layout, dashboard stub, logout mechanism
|
||||||
|
- [ ] 01-03-PLAN.md — Git push, Vercel deployment, production auth flow verification (checkpoint)
|
||||||
|
|
||||||
### Phase 2: Marketing Site
|
### Phase 2: Marketing Site
|
||||||
**Goal**: Visitors can find Teressa online, see her brand, view the listings placeholder, and submit a contact inquiry
|
**Goal**: Visitors can find Teressa online, see her brand, view the listings placeholder, and submit a contact inquiry
|
||||||
@@ -124,7 +127,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
|
|||||||
|
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Foundation | 0/? | Not started | - |
|
| 1. Foundation | 0/3 | Planned | - |
|
||||||
| 2. Marketing Site | 0/? | Not started | - |
|
| 2. Marketing Site | 0/? | Not started | - |
|
||||||
| 3. Agent Portal Shell | 0/? | Not started | - |
|
| 3. Agent Portal Shell | 0/? | Not started | - |
|
||||||
| 4. PDF Ingest | 0/? | Not started | - |
|
| 4. PDF Ingest | 0/? | Not started | - |
|
||||||
|
|||||||
440
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
440
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- package.json
|
||||||
|
- tsconfig.json
|
||||||
|
- next.config.ts
|
||||||
|
- tailwind.config.ts
|
||||||
|
- drizzle.config.ts
|
||||||
|
- src/lib/db/index.ts
|
||||||
|
- src/lib/db/schema.ts
|
||||||
|
- src/lib/auth.ts
|
||||||
|
- middleware.ts
|
||||||
|
- scripts/seed.ts
|
||||||
|
- drizzle/
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- AUTH-01
|
||||||
|
- AUTH-02
|
||||||
|
- AUTH-03
|
||||||
|
|
||||||
|
user_setup:
|
||||||
|
- service: neon
|
||||||
|
why: "PostgreSQL database for Drizzle ORM"
|
||||||
|
env_vars:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
source: "Neon console → your project → Connection string → Pooled connection (use the ?sslmode=require URL)"
|
||||||
|
dashboard_config:
|
||||||
|
- task: "Create a new Neon project named 'teressa-copeland-homes' in the us-east-1 region"
|
||||||
|
location: "https://console.neon.tech/app/new"
|
||||||
|
- service: vercel-blob
|
||||||
|
why: "File/blob storage for signed PDFs and templates (provisioned now, used in Phase 5+)"
|
||||||
|
env_vars:
|
||||||
|
- name: BLOB_READ_WRITE_TOKEN
|
||||||
|
source: "Vercel Dashboard → your project → Storage → Blob → Create Store → token appears after creation"
|
||||||
|
dashboard_config:
|
||||||
|
- task: "Create a Blob store named 'teressa-copeland-homes-blob' linked to the Vercel project"
|
||||||
|
location: "Vercel Dashboard → Storage tab"
|
||||||
|
- service: vercel
|
||||||
|
why: "Deployment host and environment variable manager"
|
||||||
|
env_vars:
|
||||||
|
- name: AUTH_SECRET
|
||||||
|
source: "Generate with: npx auth secret — copies to clipboard; paste into Vercel dashboard"
|
||||||
|
- name: AGENT_EMAIL
|
||||||
|
source: "Teressa's login email address (e.g. teressa@teressacopelandhomes.com)"
|
||||||
|
- name: AGENT_PASSWORD
|
||||||
|
source: "A strong password for Teressa's portal login"
|
||||||
|
dashboard_config:
|
||||||
|
- task: "Create a new Vercel project linked to the Git repo; set all 5 env vars (DATABASE_URL, BLOB_READ_WRITE_TOKEN, AUTH_SECRET, AGENT_EMAIL, AGENT_PASSWORD) in the project's Environment Variables settings"
|
||||||
|
location: "https://vercel.com/new"
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Next.js project builds without TypeScript errors (npm run build succeeds)"
|
||||||
|
- "Drizzle schema generates a valid SQL migration file for the users table"
|
||||||
|
- "Auth.js configuration exports handlers, auth, signIn, signOut without import errors"
|
||||||
|
- "Middleware.ts exists at project root (not inside src/) and imports from @/lib/auth"
|
||||||
|
- "Seed script creates Teressa's hashed-password account in the database when run"
|
||||||
|
- "Vercel Blob store is provisioned and BLOB_READ_WRITE_TOKEN is available"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/lib/db/schema.ts"
|
||||||
|
provides: "users table definition (id, email, password_hash, created_at)"
|
||||||
|
exports: ["users"]
|
||||||
|
- path: "src/lib/db/index.ts"
|
||||||
|
provides: "Drizzle client singleton using Neon HTTP driver"
|
||||||
|
exports: ["db"]
|
||||||
|
- path: "src/lib/auth.ts"
|
||||||
|
provides: "Auth.js v5 configuration — JWT strategy, Credentials provider, 7-day rolling session"
|
||||||
|
exports: ["handlers", "auth", "signIn", "signOut"]
|
||||||
|
- path: "middleware.ts"
|
||||||
|
provides: "Edge middleware that redirects unauthenticated /agent/* requests to /agent/login"
|
||||||
|
- path: "scripts/seed.ts"
|
||||||
|
provides: "One-time seed script that creates Teressa's account from AGENT_EMAIL + AGENT_PASSWORD env vars"
|
||||||
|
- path: "drizzle.config.ts"
|
||||||
|
provides: "Drizzle Kit configuration pointing to schema.ts and /drizzle output directory"
|
||||||
|
- path: "drizzle/"
|
||||||
|
provides: "Generated SQL migration files committed to repo"
|
||||||
|
key_links:
|
||||||
|
- from: "src/lib/auth.ts"
|
||||||
|
to: "src/lib/db"
|
||||||
|
via: "Credentials authorize() callback calls db.select(users)"
|
||||||
|
pattern: "db\\.select.*from.*users"
|
||||||
|
- from: "middleware.ts"
|
||||||
|
to: "src/lib/auth.ts"
|
||||||
|
via: "import { auth } from @/lib/auth"
|
||||||
|
pattern: "import.*auth.*from.*@/lib/auth"
|
||||||
|
- from: "scripts/seed.ts"
|
||||||
|
to: "src/lib/db/schema.ts"
|
||||||
|
via: "import { users } from ../src/lib/db/schema"
|
||||||
|
pattern: "import.*users.*from"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Scaffold the Next.js 15 project, install all Phase 1 dependencies, define the database schema, configure Drizzle ORM, write the Auth.js v5 configuration with JWT strategy + Credentials provider, create the route-protection middleware, and write the database seed script. This plan produces all the non-UI foundation contracts that Plans 02 and 03 build on top of.
|
||||||
|
|
||||||
|
Purpose: Every subsequent plan in this phase and every future phase depends on these files. Getting the contracts right here prevents cascading rework.
|
||||||
|
Output: Scaffolded project with auth.ts, schema.ts, db/index.ts, middleware.ts, seed.ts, and drizzle migration committed.
|
||||||
|
</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/STATE.md
|
||||||
|
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Contracts this plan creates — downstream plans depend on these exports. -->
|
||||||
|
<!-- Do not deviate from these shapes without updating Plans 02 and 03. -->
|
||||||
|
|
||||||
|
src/lib/db/schema.ts will export:
|
||||||
|
```typescript
|
||||||
|
export const users = pgTable("users", {
|
||||||
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
passwordHash: text("password_hash").notNull(),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
src/lib/auth.ts will export:
|
||||||
|
```typescript
|
||||||
|
export const { handlers, auth, signIn, signOut } = NextAuth({ ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
src/lib/db/index.ts will export:
|
||||||
|
```typescript
|
||||||
|
export const db = drizzle({ client: sql, schema });
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Scaffold Next.js 15 project and install all dependencies</name>
|
||||||
|
<files>package.json, tsconfig.json, next.config.ts, tailwind.config.ts, .gitignore</files>
|
||||||
|
<action>
|
||||||
|
Run the following scaffold command from the parent directory of where the project should live. If the directory `/Users/ccopeland/temp/red/teressa-copeland-homes` already exists, skip scaffolding and install missing deps into the existing project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx create-next-app@latest teressa-copeland-homes \
|
||||||
|
--typescript \
|
||||||
|
--tailwind \
|
||||||
|
--eslint \
|
||||||
|
--app \
|
||||||
|
--src-dir \
|
||||||
|
--import-alias "@/*" \
|
||||||
|
--no-git
|
||||||
|
```
|
||||||
|
|
||||||
|
After scaffolding (cd into the project directory), install Phase 1 dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auth + DB + Blob
|
||||||
|
npm install next-auth@beta
|
||||||
|
npm install drizzle-orm @neondatabase/serverless
|
||||||
|
npm install bcryptjs @vercel/blob zod
|
||||||
|
npm install -D drizzle-kit tsx @types/bcryptjs dotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
After install, note the exact installed `next-auth` version with `npm ls next-auth` and pin it in package.json (replace `"next-auth": "beta"` with the exact version string like `"next-auth": "5.0.0-beta.25"`).
|
||||||
|
|
||||||
|
Add these npm scripts to package.json:
|
||||||
|
```json
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:seed": "tsx scripts/seed.ts",
|
||||||
|
"db:studio": "drizzle-kit studio"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure `.gitignore` includes:
|
||||||
|
```
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env*.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Do NOT create a `.env` file or `.env.local` file — all secrets go through Vercel dashboard per user decision.
|
||||||
|
|
||||||
|
Confirm `npm run build` succeeds (will show only placeholder page errors, not import errors).
|
||||||
|
</action>
|
||||||
|
<verify>npm run build passes with no TypeScript compilation errors; npm ls next-auth shows a pinned specific version (not "beta")</verify>
|
||||||
|
<done>Project directory exists, all dependencies installed, exact next-auth version pinned in package.json, npm run build succeeds</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Define database schema, configure Drizzle Kit, and write seed script</name>
|
||||||
|
<files>src/lib/db/schema.ts, src/lib/db/index.ts, drizzle.config.ts, scripts/seed.ts, drizzle/ (generated)</files>
|
||||||
|
<action>
|
||||||
|
Create `drizzle.config.ts` at the project root:
|
||||||
|
```typescript
|
||||||
|
import "dotenv/config";
|
||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./src/lib/db/schema.ts",
|
||||||
|
out: "./drizzle",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/lib/db/schema.ts`:
|
||||||
|
```typescript
|
||||||
|
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const users = pgTable("users", {
|
||||||
|
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
passwordHash: text("password_hash").notNull(),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/lib/db/index.ts`:
|
||||||
|
```typescript
|
||||||
|
import { drizzle } from "drizzle-orm/neon-http";
|
||||||
|
import { neon } from "@neondatabase/serverless";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
const sql = neon(process.env.DATABASE_URL!);
|
||||||
|
export const db = drizzle({ client: sql, schema });
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `scripts/seed.ts`:
|
||||||
|
```typescript
|
||||||
|
import "dotenv/config";
|
||||||
|
import { drizzle } from "drizzle-orm/neon-http";
|
||||||
|
import { neon } from "@neondatabase/serverless";
|
||||||
|
import { users } from "../src/lib/db/schema";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
const sql = neon(process.env.DATABASE_URL!);
|
||||||
|
const db = drizzle({ client: sql });
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
const email = process.env.AGENT_EMAIL;
|
||||||
|
const password = process.env.AGENT_PASSWORD;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new Error("AGENT_EMAIL and AGENT_PASSWORD env vars are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
|
await db.insert(users).values({ email, passwordHash }).onConflictDoNothing();
|
||||||
|
|
||||||
|
console.log(`Seeded agent account: ${email}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
seed().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate migration files (requires DATABASE_URL to be set — if not yet available, generate schema only and note that `db:migrate` and `db:seed` run after user sets up Neon):
|
||||||
|
```bash
|
||||||
|
# If DATABASE_URL is available locally (via `vercel env pull` after Vercel project creation):
|
||||||
|
npm run db:generate # Creates drizzle/0000_*.sql migration file
|
||||||
|
npm run db:migrate # Applies migration to Neon database
|
||||||
|
npm run db:seed # Creates Teressa's account
|
||||||
|
|
||||||
|
# If DATABASE_URL is NOT yet available:
|
||||||
|
# Generate migration file only (does not need DB connection):
|
||||||
|
npm run db:generate
|
||||||
|
# Note in SUMMARY.md that db:migrate and db:seed must run after user sets up Neon
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated `/drizzle` directory with SQL migration files MUST be committed to the repo (per user decision — version-controlled, auditable migrations).
|
||||||
|
</action>
|
||||||
|
<verify>drizzle/ directory contains at least one .sql migration file; src/lib/db/schema.ts and index.ts export users and db without TypeScript errors (npx tsc --noEmit)</verify>
|
||||||
|
<done>Schema file exports users table, db/index.ts exports db singleton, drizzle.config.ts present, seed script present, migration SQL file exists in /drizzle and committed to git</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Configure Auth.js v5 with JWT strategy and create route-protection middleware</name>
|
||||||
|
<files>src/lib/auth.ts, src/app/api/auth/[...nextauth]/route.ts, middleware.ts</files>
|
||||||
|
<action>
|
||||||
|
Create `src/lib/auth.ts` — the single source of truth for authentication configuration:
|
||||||
|
```typescript
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
import Credentials from "next-auth/providers/credentials";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { users } from "@/lib/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 7 * 24 * 60 * 60, // 7 days — rolling session per user decision
|
||||||
|
updateAge: 24 * 60 * 60, // Refresh cookie once per day (not every request)
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: "/agent/login", // Custom login page — per user decision
|
||||||
|
},
|
||||||
|
providers: [
|
||||||
|
Credentials({
|
||||||
|
async authorize(credentials) {
|
||||||
|
const parsed = loginSchema.safeParse(credentials);
|
||||||
|
if (!parsed.success) return null;
|
||||||
|
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, parsed.data.email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const passwordMatch = await bcrypt.compare(
|
||||||
|
parsed.data.password,
|
||||||
|
user.passwordHash
|
||||||
|
);
|
||||||
|
if (!passwordMatch) return null;
|
||||||
|
|
||||||
|
return { id: user.id, email: user.email };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
jwt({ token, user }) {
|
||||||
|
if (user) token.id = user.id;
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
session({ session, token }) {
|
||||||
|
session.user.id = token.id as string;
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `src/app/api/auth/[...nextauth]/route.ts`:
|
||||||
|
```typescript
|
||||||
|
import { handlers } from "@/lib/auth";
|
||||||
|
export const { GET, POST } = handlers;
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `middleware.ts` at the PROJECT ROOT (not inside src/ — critical, middleware placed in src/ is silently ignored by Next.js):
|
||||||
|
```typescript
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export default auth((req) => {
|
||||||
|
const isLoggedIn = !!req.auth;
|
||||||
|
const isAgentRoute = req.nextUrl.pathname.startsWith("/agent");
|
||||||
|
const isLoginPage = req.nextUrl.pathname === "/agent/login";
|
||||||
|
|
||||||
|
// Protect all /agent/* routes except the login page itself
|
||||||
|
if (isAgentRoute && !isLoginPage && !isLoggedIn) {
|
||||||
|
return NextResponse.redirect(new URL("/agent/login", req.nextUrl.origin));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect already-authenticated users away from the login page
|
||||||
|
if (isLoginPage && isLoggedIn) {
|
||||||
|
return NextResponse.redirect(new URL("/agent/dashboard", req.nextUrl.origin));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
// Run on /agent/* routes, excluding Next.js internals and static files
|
||||||
|
"/((?!_next/static|_next/image|favicon.ico).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `middleware.ts` is at the same level as `package.json` — NOT inside `src/`. This is the single most common pitfall: middleware inside src/ is silently ignored and routes are unprotected.
|
||||||
|
|
||||||
|
AUTH_SECRET env var must be set for Auth.js to function. The user will add this to Vercel dashboard. For local development, note this requirement in SUMMARY.md — `vercel env pull` will populate `.env.local` once the Vercel project is created.
|
||||||
|
</action>
|
||||||
|
<verify>npx tsc --noEmit passes with no errors in auth.ts, middleware.ts, and route.ts; middleware.ts file exists at project root (same level as package.json), NOT inside src/</verify>
|
||||||
|
<done>src/lib/auth.ts exports handlers, auth, signIn, signOut; API route handler exists at src/app/api/auth/[...nextauth]/route.ts; middleware.ts exists at project root with /agent/* protection logic</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Run these commands from the project root directory after completing all tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# TypeScript: zero errors across all created files
|
||||||
|
npx tsc --noEmit
|
||||||
|
|
||||||
|
# Build: confirms Next.js can compile the project
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Migration file exists (committed to git)
|
||||||
|
ls drizzle/*.sql
|
||||||
|
|
||||||
|
# Middleware location (MUST be at root, not src/)
|
||||||
|
ls middleware.ts && echo "OK" || echo "MISSING — check location"
|
||||||
|
ls src/middleware.ts 2>/dev/null && echo "WRONG LOCATION" || echo "Correctly absent from src/"
|
||||||
|
|
||||||
|
# Auth exports are importable (no circular deps or missing modules)
|
||||||
|
node -e "import('./src/lib/auth.ts').then(() => console.log('auth.ts OK')).catch(e => console.error(e.message))"
|
||||||
|
```
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- npm run build succeeds with no TypeScript errors
|
||||||
|
- drizzle/ directory contains at least one committed SQL migration file
|
||||||
|
- middleware.ts exists at project root, NOT inside src/
|
||||||
|
- src/lib/auth.ts exports handlers, auth, signIn, signOut (JWT strategy, 7-day maxAge, Credentials provider)
|
||||||
|
- src/lib/db/schema.ts exports users table with id, email, passwordHash, createdAt columns
|
||||||
|
- src/lib/db/index.ts exports db (Drizzle + Neon HTTP driver singleton)
|
||||||
|
- src/app/api/auth/[...nextauth]/route.ts exports GET and POST
|
||||||
|
- scripts/seed.ts reads AGENT_EMAIL and AGENT_PASSWORD from env and creates hashed user row
|
||||||
|
- next-auth version is pinned to a specific beta version (not "beta" or "^5...")
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` using the summary template.
|
||||||
|
|
||||||
|
Include in the summary:
|
||||||
|
- Exact next-auth beta version pinned
|
||||||
|
- Whether db:migrate and db:seed were run successfully (or if they are pending user Neon setup)
|
||||||
|
- Location of middleware.ts confirmed (root vs src/)
|
||||||
|
- Any deviations from the planned patterns and why
|
||||||
|
</output>
|
||||||
348
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
348
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
215
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
215
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 3
|
||||||
|
depends_on:
|
||||||
|
- "01-01"
|
||||||
|
- "01-02"
|
||||||
|
files_modified: []
|
||||||
|
autonomous: false
|
||||||
|
requirements:
|
||||||
|
- AUTH-01
|
||||||
|
- AUTH-02
|
||||||
|
- AUTH-03
|
||||||
|
- AUTH-04
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Project is deployed and live at a Vercel URL (teressacopelandhomes.com or a *.vercel.app preview URL)"
|
||||||
|
- "Agent can log in with email and password in the production deployment"
|
||||||
|
- "Agent session persists after browser tab close and reopen"
|
||||||
|
- "Visiting any /agent/* route while logged out redirects to /agent/login"
|
||||||
|
- "Agent can log out and sees 'You've been signed out'"
|
||||||
|
- "Database schema is live in Neon with Teressa's seeded account"
|
||||||
|
- "Vercel Blob store exists and BLOB_READ_WRITE_TOKEN is set in Vercel environment"
|
||||||
|
artifacts:
|
||||||
|
- path: "Vercel project"
|
||||||
|
provides: "Production deployment with all 5 env vars set"
|
||||||
|
- path: "Neon database"
|
||||||
|
provides: "users table with Teressa's seeded account"
|
||||||
|
- path: "Vercel Blob store"
|
||||||
|
provides: "Single blob store for future document storage (Phase 4+)"
|
||||||
|
key_links:
|
||||||
|
- from: "Vercel deployment"
|
||||||
|
to: "Neon database"
|
||||||
|
via: "DATABASE_URL env var in Vercel project settings"
|
||||||
|
pattern: "DATABASE_URL set in Vercel env"
|
||||||
|
- from: "Vercel deployment"
|
||||||
|
to: "Auth.js"
|
||||||
|
via: "AUTH_SECRET env var in Vercel project settings"
|
||||||
|
pattern: "AUTH_SECRET set in Vercel env"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Deploy the project to Vercel, wire all environment variables, run the database migration and seed on the production Neon database, and verify the complete auth flow end-to-end in the live deployment. This plan is a blocking checkpoint — a human must confirm the production deployment works before Phase 1 is complete.
|
||||||
|
|
||||||
|
Purpose: Phase 1's success criterion explicitly requires the project to be deployed to Vercel with environment variables wired. This checkpoint ensures that is true before declaring Phase 1 done.
|
||||||
|
Output: Live production deployment of teressacopelandhomes.com (or *.vercel.app) with working auth.
|
||||||
|
</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/ROADMAP.md
|
||||||
|
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-foundation/01-01-SUMMARY.md
|
||||||
|
@.planning/phases/01-foundation/01-02-SUMMARY.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Push to Git and verify Vercel auto-deploy triggers</name>
|
||||||
|
<files></files>
|
||||||
|
<action>
|
||||||
|
Initialize a git repo if not already done, then push to the remote that Vercel's Git integration watches:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the project root directory:
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "feat(01-foundation): Next.js scaffold, auth, database schema, and login UI"
|
||||||
|
git branch -M main
|
||||||
|
git remote add origin <your-github-repo-url>
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
Per user decision: Vercel native Git integration is used (push to main → auto-deploy). GitHub Actions are NOT used.
|
||||||
|
|
||||||
|
After pushing:
|
||||||
|
1. Open the Vercel dashboard for the project
|
||||||
|
2. Watch the deployment in the "Deployments" tab — it should trigger automatically
|
||||||
|
3. If the deployment FAILS because of missing env vars (expected on first deploy), that is fine — the checkpoint task below addresses this
|
||||||
|
4. Note the Vercel project URL (either custom domain or *.vercel.app URL)
|
||||||
|
|
||||||
|
IMPORTANT: If the Vercel project was not yet created (user setup not yet done), the push will not trigger a deployment. Claude cannot create the Vercel project or set env vars — the user_setup block in Plan 01's frontmatter lists these steps. Only proceed here if the user confirms setup is complete.
|
||||||
|
|
||||||
|
If user setup IS complete and the deployment succeeds:
|
||||||
|
```bash
|
||||||
|
# Pull env vars for local use (creates .env.local — already in .gitignore)
|
||||||
|
vercel env pull
|
||||||
|
|
||||||
|
# Run migration against production Neon database
|
||||||
|
npm run db:migrate
|
||||||
|
|
||||||
|
# Seed Teressa's account
|
||||||
|
npm run db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
If db:migrate and db:seed were already run locally against Neon during Plan 01 (because DATABASE_URL was available early), skip running them again — the onConflictDoNothing in seed.ts makes re-runs safe but unnecessary.
|
||||||
|
</action>
|
||||||
|
<verify>Vercel dashboard shows deployment status; git push completes without error; vercel env pull creates a .env.local file with all 5 required env vars (DATABASE_URL, AUTH_SECRET, BLOB_READ_WRITE_TOKEN, AGENT_EMAIL, AGENT_PASSWORD)</verify>
|
||||||
|
<done>Code is pushed to main, Vercel deployment has triggered (or completed), .env.local populated via vercel env pull</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 2: Verify complete auth flow in production</name>
|
||||||
|
<files></files>
|
||||||
|
<action>Human verification of the complete Phase 1 auth flow in the live Vercel production deployment. See how-to-verify for the 7 test cases.</action>
|
||||||
|
<what-built>
|
||||||
|
Complete Phase 1 auth system:
|
||||||
|
- Branded login page at /agent/login with password toggle
|
||||||
|
- Protected agent dashboard at /agent/dashboard
|
||||||
|
- 7-day rolling JWT session (persistent across browser restarts)
|
||||||
|
- Middleware-enforced route protection for all /agent/* routes
|
||||||
|
- Logout with "You've been signed out" confirmation
|
||||||
|
- Neon database with users table and Teressa's seeded account
|
||||||
|
- Vercel Blob store provisioned
|
||||||
|
- Deployed to Vercel with all environment variables wired
|
||||||
|
</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
Open the production URL from the Vercel dashboard (either https://teressacopelandhomes.com or the *.vercel.app preview URL).
|
||||||
|
|
||||||
|
**Test 1 — Route protection (AUTH-03):**
|
||||||
|
- Navigate directly to: `[your-vercel-url]/agent/dashboard`
|
||||||
|
- Expected: Redirected to `/agent/login` (not a 404 or blank page)
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
**Test 2 — Login with wrong credentials (AUTH-01):**
|
||||||
|
- On /agent/login, enter any email + wrong password
|
||||||
|
- Expected: Page reloads, shows "Invalid email or password" banner
|
||||||
|
- The error should NOT say which field is wrong
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
**Test 3 — Login with correct credentials (AUTH-01):**
|
||||||
|
- On /agent/login, enter Teressa's AGENT_EMAIL and AGENT_PASSWORD
|
||||||
|
- Expected: Redirects to /agent/dashboard showing Teressa's email address
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
**Test 4 — Session persistence (AUTH-02):**
|
||||||
|
- After logging in, close the browser tab (or close and reopen the browser window)
|
||||||
|
- Navigate back to `[your-vercel-url]/agent/dashboard`
|
||||||
|
- Expected: Still logged in — dashboard shows without redirecting to login
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
**Test 5 — Logout (AUTH-04):**
|
||||||
|
- On /agent/dashboard, click "Sign out"
|
||||||
|
- Expected: Redirected to /agent/login with a "You've been signed out" message visible on the page
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
**Test 6 — Post-logout protection (AUTH-03):**
|
||||||
|
- After logging out, navigate to `[your-vercel-url]/agent/dashboard`
|
||||||
|
- Expected: Redirected to /agent/login (session is invalidated)
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
**Test 7 — Password toggle:**
|
||||||
|
- On /agent/login, type any password in the password field
|
||||||
|
- Click the show/hide toggle
|
||||||
|
- Expected: Password becomes visible as plain text
|
||||||
|
- Pass / Fail?
|
||||||
|
|
||||||
|
If all 7 tests pass: type "approved" to complete Phase 1.
|
||||||
|
If any test fails: describe which test failed and what you saw.
|
||||||
|
</how-to-verify>
|
||||||
|
<verify>All 7 tests pass and human types "approved"</verify>
|
||||||
|
<done>Human has approved all 7 auth flow tests in the production Vercel deployment</done>
|
||||||
|
<resume-signal>Type "approved" if all 7 tests pass, or describe failures</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
All verification is human-driven in the checkpoint above. The 7 tests map directly to the Phase 1 success criteria from ROADMAP.md:
|
||||||
|
|
||||||
|
| ROADMAP Criterion | Tests |
|
||||||
|
|-------------------|-------|
|
||||||
|
| Agent can log in with email and password | Tests 2 + 3 |
|
||||||
|
| Session persists after browser refresh/tab close | Test 4 |
|
||||||
|
| Unauthenticated /agent/* routes redirect to login | Tests 1 + 6 |
|
||||||
|
| Agent can log out | Test 5 |
|
||||||
|
| Database deployed to Neon, Blob created, Vercel wired | Implicit in Tests 3 + 4 working in production |
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
All 7 verification tests pass in the production Vercel deployment:
|
||||||
|
1. /agent/dashboard redirects unauthenticated users to /agent/login
|
||||||
|
2. Wrong credentials show "Invalid email or password"
|
||||||
|
3. Correct credentials grant access to /agent/dashboard
|
||||||
|
4. Session survives browser tab close and reopen
|
||||||
|
5. Logout redirects to /agent/login with confirmation message
|
||||||
|
6. Post-logout /agent/dashboard visit redirects to login
|
||||||
|
7. Password toggle switches between hidden/visible
|
||||||
|
|
||||||
|
Human approves the checkpoint with "approved".
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After human approval, create `.planning/phases/01-foundation/01-03-SUMMARY.md` using the summary template.
|
||||||
|
|
||||||
|
Include in the summary:
|
||||||
|
- Production deployment URL
|
||||||
|
- Confirmation that all 7 verification tests passed
|
||||||
|
- Neon project name and region
|
||||||
|
- Whether db:migrate and db:seed ran successfully
|
||||||
|
- Vercel Blob store name
|
||||||
|
- Any issues encountered during deployment and how they were resolved
|
||||||
|
|
||||||
|
Then update STATE.md:
|
||||||
|
- Current focus: Phase 2 - Marketing Site
|
||||||
|
- Phase 1 status: Complete
|
||||||
|
- Last activity: [today's date] — Phase 1 complete; auth flow verified in production
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user