8.9 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-agent-portal-shell | 01 | execute | 1 |
|
true |
|
|
Purpose: Every subsequent Phase 3 plan depends on these two tables and on the /portal route prefix being protected. This plan lays the data and routing foundations.
Output: Working Drizzle migration for clients + documents tables; middleware protecting /portal/*; post-login redirect updated.
<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/STATE.md @.planning/phases/03-agent-portal-shell/03-CONTEXT.md @.planning/phases/03-agent-portal-shell/03-RESEARCH.mdExisting users table (for reference pattern):
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
// users table already exists — do NOT redefine it
// ADD below the existing users table:
export const clients = pgTable("clients", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
email: text("email").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
export const documentStatusEnum = pgEnum("document_status", [
"Draft", "Sent", "Viewed", "Signed"
]);
export const documents = pgTable("documents", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text("name").notNull(),
clientId: text("client_id").notNull().references(() => clients.id, { onDelete: "cascade" }),
status: documentStatusEnum("status").notNull().default("Draft"),
sentAt: timestamp("sent_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Existing middleware.ts pattern (matcher array to extend):
export const config = {
matcher: ["/agent/:path*"], // ADD "/portal/:path*" here
};
Existing auth.config.ts authorized callback (redirect to update):
// Change: redirect(new URL("/agent/dashboard", nextUrl))
// To: redirect(new URL("/portal/dashboard", nextUrl))
// Also add: nextUrl.pathname.startsWith("/portal") to protected routes check
- Import
pgEnumfromdrizzle-orm/pg-core(add to existing import). - Export
documentStatusEnumusingpgEnum("document_status", ["Draft", "Sent", "Viewed", "Signed"]). - Export
clientstable: id (text PK with crypto.randomUUID), name (text not null), email (text not null), createdAt (timestamp defaultNow not null), updatedAt (timestamp defaultNow not null). - Export
documentstable (stub — no PDF content yet): id (text PK with crypto.randomUUID), name (text not null), clientId (text not null, FK to clients.id with onDelete: "cascade"), status (documentStatusEnum column default "Draft" not null), sentAt (timestamp nullable), createdAt (timestamp defaultNow not null).
CRITICAL: Export the enum BEFORE the documents table (it is referenced by the documents table column). Export both tables so they are importable by other modules.
Then generate and run the migration:
cd teressa-copeland-homes && npm run db:generate && npm run db:migrate
The migration SQL file will be created automatically in the drizzle/ directory. Do NOT hand-write SQL.
PITFALL: pgEnum must be exported AND referenced by a table column or drizzle-kit generate may omit it. Confirm the generated SQL contains CREATE TYPE document_status AS ENUM.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate && npm run db:migrate 2>&1 | tail -20
Migration runs without error. clients and documents tables exist in the database. document_status enum exists in PostgreSQL. schema.ts exports clients, documents, and documentStatusEnum.
auth.config.ts: Read the file. Make two changes:
- In the
authorizedcallback, extend the protected route check to also cover/portalroutes:// Add alongside the existing /agent check: if (nextUrl.pathname.startsWith("/portal")) { if (!isLoggedIn) return Response.redirect(new URL("/agent/login", nextUrl)); } - Change the post-login redirect (where a logged-in user visiting the login page is sent) from
/agent/dashboardto/portal/dashboard:// Change: new URL("/agent/dashboard", nextUrl) // To: new URL("/portal/dashboard", nextUrl)
PITFALL (from RESEARCH.md): The authorized callback in Auth.js v5 beta may handle the redirect differently than a plain middleware — read the existing callback logic carefully and mirror the existing /agent protection pattern rather than rewriting it.
src/app/agent/(protected)/dashboard/page.tsx: Replace the dashboard stub with a redirect to /portal/dashboard. Import redirect from next/navigation and call redirect("/portal/dashboard") as the only content. This ensures any old link to /agent/dashboard silently forwards to the new location.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1
TypeScript compiles with zero errors. middleware.ts matcher includes /portal/:path*. auth.config.ts post-login redirect is /portal/dashboard. /agent/dashboard/page.tsx redirects to /portal/dashboard.
<success_criteria>
- Drizzle migration creates
clientsanddocumentstables anddocument_statusenum in PostgreSQL - All
/portal/routes require authentication (middleware protects them) - After login, agent is redirected to
/portal/dashboard - TypeScript compiles cleanly </success_criteria>