200 lines
8.9 KiB
Markdown
200 lines
8.9 KiB
Markdown
---
|
|
phase: 03-agent-portal-shell
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- teressa-copeland-homes/src/lib/db/schema.ts
|
|
- teressa-copeland-homes/middleware.ts
|
|
- teressa-copeland-homes/src/lib/auth.config.ts
|
|
- teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
|
|
- teressa-copeland-homes/drizzle/0001_clients_documents.sql
|
|
autonomous: true
|
|
requirements: [CLIENT-01, CLIENT-02, CLIENT-03, DASH-01, DASH-02]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Running `npm run db:generate && npm run db:migrate` produces a migration that creates the `clients` and `documents` tables with no errors"
|
|
- "Visiting /portal/dashboard while unauthenticated redirects to /agent/login — not a 404 or blank page"
|
|
- "After login, agent is redirected to /portal/dashboard (not /agent/dashboard)"
|
|
- "The document_status PostgreSQL enum exists with values Draft, Sent, Viewed, Signed"
|
|
artifacts:
|
|
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
|
|
provides: "clients and documents table definitions + documentStatusEnum"
|
|
contains: "pgTable(\"clients\""
|
|
- path: "teressa-copeland-homes/middleware.ts"
|
|
provides: "Route protection for /portal/:path*"
|
|
contains: "/portal/:path*"
|
|
- path: "teressa-copeland-homes/src/lib/auth.config.ts"
|
|
provides: "Post-login redirect to /portal/dashboard"
|
|
contains: "/portal/dashboard"
|
|
key_links:
|
|
- from: "middleware.ts matcher"
|
|
to: "/portal/:path* routes"
|
|
via: "matcher array"
|
|
pattern: "portal.*path"
|
|
- from: "auth.config.ts authorized callback"
|
|
to: "session check for /portal routes"
|
|
via: "nextUrl.pathname.startsWith('/portal')"
|
|
pattern: "startsWith.*portal"
|
|
---
|
|
|
|
<objective>
|
|
Add the `clients` and `documents` stub tables to the Drizzle schema, generate and run the migration, and update the middleware + auth config so all `/portal/` routes are protected and post-login redirect lands on `/portal/dashboard`.
|
|
|
|
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.
|
|
</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/03-agent-portal-shell/03-CONTEXT.md
|
|
@.planning/phases/03-agent-portal-shell/03-RESEARCH.md
|
|
|
|
<interfaces>
|
|
<!-- Existing schema pattern (from src/lib/db/schema.ts) -->
|
|
<!-- Executor: match this exact style for new tables -->
|
|
|
|
Existing users table (for reference pattern):
|
|
```typescript
|
|
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):
|
|
```typescript
|
|
export const config = {
|
|
matcher: ["/agent/:path*"], // ADD "/portal/:path*" here
|
|
};
|
|
```
|
|
|
|
Existing auth.config.ts authorized callback (redirect to update):
|
|
```typescript
|
|
// Change: redirect(new URL("/agent/dashboard", nextUrl))
|
|
// To: redirect(new URL("/portal/dashboard", nextUrl))
|
|
// Also add: nextUrl.pathname.startsWith("/portal") to protected routes check
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Extend Drizzle schema with clients and documents tables</name>
|
|
<files>teressa-copeland-homes/src/lib/db/schema.ts</files>
|
|
<action>
|
|
Read the current schema.ts first. Then add, below the existing users table definition:
|
|
|
|
1. Import `pgEnum` from `drizzle-orm/pg-core` (add to existing import).
|
|
2. Export `documentStatusEnum` using `pgEnum("document_status", ["Draft", "Sent", "Viewed", "Signed"])`.
|
|
3. Export `clients` table: id (text PK with crypto.randomUUID), name (text not null), email (text not null), createdAt (timestamp defaultNow not null), updatedAt (timestamp defaultNow not null).
|
|
4. Export `documents` table (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`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate && npm run db:migrate 2>&1 | tail -20</automated>
|
|
</verify>
|
|
<done>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`.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Update middleware and auth config to protect /portal routes</name>
|
|
<files>
|
|
teressa-copeland-homes/middleware.ts
|
|
teressa-copeland-homes/src/lib/auth.config.ts
|
|
teressa-copeland-homes/src/app/agent/(protected)/dashboard/page.tsx
|
|
</files>
|
|
<action>
|
|
**middleware.ts**: Read the file. Change the matcher to include both `/agent/:path*` and `/portal/:path*`:
|
|
```typescript
|
|
export const config = {
|
|
matcher: ["/agent/:path*", "/portal/:path*"],
|
|
};
|
|
```
|
|
|
|
**auth.config.ts**: Read the file. Make two changes:
|
|
1. In the `authorized` callback, extend the protected route check to also cover `/portal` routes:
|
|
```typescript
|
|
// Add alongside the existing /agent check:
|
|
if (nextUrl.pathname.startsWith("/portal")) {
|
|
if (!isLoggedIn) return Response.redirect(new URL("/agent/login", nextUrl));
|
|
}
|
|
```
|
|
2. Change the post-login redirect (where a logged-in user visiting the login page is sent) from `/agent/dashboard` to `/portal/dashboard`:
|
|
```typescript
|
|
// 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1</automated>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. `npm run db:generate && npm run db:migrate` completes without error
|
|
2. `npx tsc --noEmit` produces zero TypeScript errors
|
|
3. schema.ts contains exports for `clients`, `documents`, and `documentStatusEnum`
|
|
4. middleware.ts matcher array contains `"/portal/:path*"`
|
|
5. auth.config.ts contains `startsWith("/portal")` in protected route check
|
|
6. auth.config.ts post-login redirect is `/portal/dashboard`
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Drizzle migration creates `clients` and `documents` tables and `document_status` enum in PostgreSQL
|
|
- All `/portal/` routes require authentication (middleware protects them)
|
|
- After login, agent is redirected to `/portal/dashboard`
|
|
- TypeScript compiles cleanly
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-agent-portal-shell/03-01-SUMMARY.md`
|
|
</output>
|