11 KiB
Username Password Auth Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build only username/password register-login with HttpOnly Cookie session, plus logout and current-user APIs.
Architecture: Keep auth logic in server/service/auth/index.ts, expose four thin API handlers under server/api/auth/, and persist sessions in PostgreSQL via Drizzle schema. Frontend only updates app/pages/login/index.vue and adds app/pages/register/index.vue to call real APIs; no extra auth features are added.
Tech Stack: Nuxt 4 + Nitro server routes, Drizzle ORM (PostgreSQL), Bun scripts, Nuxt UI form components.
File Structure Map
- Modify:
packages/drizzle-pkg/database/pg/schema/auth.ts(addsessionstable) - Modify:
packages/drizzle-pkg/lib/schema/auth.ts(exportsessions) - Create:
server/api/auth/register.post.ts - Create:
server/api/auth/login.post.ts - Create:
server/api/auth/logout.post.ts - Create:
server/api/auth/me.get.ts - Modify:
server/service/auth/index.ts(register/login/logout/me service functions) - Modify:
app/pages/login/index.vue(username+password real API) - Create:
app/pages/register/index.vue
Task 1: Prepare DB session model
Files:
-
Modify:
packages/drizzle-pkg/database/pg/schema/auth.ts -
Modify:
packages/drizzle-pkg/lib/schema/auth.ts -
Test:
packages/drizzle-pkg/database/pg/schema/auth.ts(type-check + migration generation output) -
Step 1: Write the failing schema expectation
// Expected new table in auth schema:
// export const sessions = pgTable("sessions", { ... })
//
// Expected exported symbol:
// export { users, sessions } from '../../database/pg/schema/auth'
- Step 2: Verify current state is missing sessions
Run: rg "sessions" packages/drizzle-pkg/database/pg/schema/auth.ts packages/drizzle-pkg/lib/schema/auth.ts
Expected: no sessions table export in current output.
- Step 3: Add minimal schema implementation
// packages/drizzle-pkg/database/pg/schema/auth.ts
import { sql } from "drizzle-orm";
import { integer, pgTable, timestamp, varchar } from "drizzle-orm/pg-core";
export const sessions = pgTable("sessions", {
id: varchar().primaryKey(),
userId: integer().notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// packages/drizzle-pkg/lib/schema/auth.ts
export { users, sessions } from "../../database/pg/schema/auth";
- Step 4: Generate migration and verify
Run: bun run db:generate -- auth-sessions
Expected: new migration created for sessions table with correct columns.
- Step 5: Commit
git add packages/drizzle-pkg/database/pg/schema/auth.ts packages/drizzle-pkg/lib/schema/auth.ts packages/drizzle-pkg/database/pg/migrations
git commit -m "feat: add sessions schema for auth"
Task 2: Implement auth service (register/login/logout/me)
Files:
-
Modify:
server/service/auth/index.ts -
Test:
server/service/auth/index.ts(manual function-level behavior check via API handlers in next task) -
Step 1: Write the failing behavior checklist
// register(username, password)
// - validates username /^[a-zA-Z0-9_]{3,20}$/
// - validates password length >= 6
// - throws on duplicate username
//
// login(username, password)
// - validates credentials
// - returns session payload (sessionId, expiresAt, user)
//
// logout(sessionId)
// - deletes session record
//
// getMe(sessionId)
// - returns null when missing/expired
// - returns { id, username } when valid
- Step 2: Confirm current service cannot satisfy checklist
Run: rg "register|login|logout|getMe" server/service/auth/index.ts
Expected: these functions are absent.
- Step 3: Add minimal implementation
// server/service/auth/index.ts (key shape)
export async function registerUser(input: { username: string; password: string }) { /* validate + insert */ }
export async function loginUser(input: { username: string; password: string }) { /* verify + create session */ }
export async function logoutUser(sessionId: string) { /* delete session */ }
export async function getCurrentUser(sessionId: string) { /* validate session + fetch user */ }
Implementation notes in this step:
-
password hashing/comparison:
bun add bcryptjsthen usehash/compare. -
session id generation:
crypto.randomUUID(). -
session expiry:
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000). -
return only minimal user fields.
-
Step 4: Run type check/dev build sanity
Run: bun run build
Expected: build completes without TypeScript errors from auth service changes.
- Step 5: Commit
git add server/service/auth/index.ts package.json bun.lock
git commit -m "feat: implement auth service for username-password flow"
Task 3: Add auth API routes with HttpOnly Cookie
Files:
-
Create:
server/api/auth/register.post.ts -
Create:
server/api/auth/login.post.ts -
Create:
server/api/auth/logout.post.ts -
Create:
server/api/auth/me.get.ts -
Test: these four route files through local HTTP calls
-
Step 1: Write failing API contract examples
// register.post.ts: accepts { username, password }, returns success json
// login.post.ts: accepts { username, password }, sets cookie "pp_session"
// me.get.ts: reads cookie, returns current user or unauthorized
// logout.post.ts: clears cookie and invalidates session
- Step 2: Verify routes do not yet exist
Run: ls server/api/auth
Expected: folder/files missing (or missing required four handlers).
- Step 3: Implement handlers with thin controller pattern
// login.post.ts (core shape)
const body = await readBody(event);
const result = await loginUser(body);
setCookie(event, "pp_session", result.sessionId, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 60 * 60 * 24 * 7,
});
return { code: 1, message: "登录成功", data: result.user };
// me.get.ts (core shape)
const sessionId = getCookie(event, "pp_session");
const user = await getCurrentUser(sessionId ?? "");
if (!user) throw createError({ statusCode: 401, statusMessage: "未登录" });
return { code: 1, message: "ok", data: user };
- Step 4: Run API smoke checks
Run:
bun run dev then in another terminal:
curl -i -X POST http://localhost:3000/api/auth/register \
-H "content-type: application/json" \
-d '{"username":"demo_user","password":"123456"}'
curl -i -c cookie.txt -X POST http://localhost:3000/api/auth/login \
-H "content-type: application/json" \
-d '{"username":"demo_user","password":"123456"}'
curl -i -b cookie.txt http://localhost:3000/api/auth/me
curl -i -b cookie.txt -X POST http://localhost:3000/api/auth/logout
Expected: register/login/me/logout all return success on happy path; Set-Cookie appears on login.
- Step 5: Commit
git add server/api/auth
git commit -m "feat: add auth api routes with cookie session"
Task 4: Update login page to username/password real login
Files:
-
Modify:
app/pages/login/index.vue -
Test: login page manual browser check
-
Step 1: Write failing UI expectation
<!-- expected -->
<!-- form uses username field (not email), submits to /api/auth/login -->
<!-- success message based on actual API response -->
- Step 2: Confirm current UI still email + mock delay
Run: rg "email|setTimeout|you@example.com" app/pages/login/index.vue
Expected: current file still uses email and simulated success.
- Step 3: Implement minimal page changes
type LoginFormState = { username: string; password: string };
const res = await $fetch("/api/auth/login", { method: "POST", body: state });
resultType.value = "success";
resultMessage.value = `登录成功,欢迎 ${res.data.username}`;
<UFormField label="用户名" name="username" required>
<UInput v-model="state.username" autocomplete="username" />
</UFormField>
- Step 4: Verify behavior in browser
Run: bun run dev and open /login
Expected: validation matches username/password rules; successful login calls backend and shows success.
- Step 5: Commit
git add app/pages/login/index.vue
git commit -m "feat: wire login page to username-password auth api"
Task 5: Add register page and complete acceptance checklist
Files:
-
Create:
app/pages/register/index.vue -
Test: end-to-end acceptance via browser and curl
-
Step 1: Write failing acceptance checklist
- register success
- duplicate username rejected
- login success sets cookie
- invalid credentials rejected
- me returns user when logged in
- logout clears session
- Step 2: Confirm register page is missing
Run: ls app/pages/register/index.vue
Expected: file not found.
- Step 3: Implement minimal register page
// app/pages/register/index.vue (core submit)
await $fetch("/api/auth/register", { method: "POST", body: state });
await navigateTo("/login");
<!-- fields -->
<UFormField label="用户名" name="username" required />
<UFormField label="密码" name="password" required />
- Step 4: Execute full acceptance checks
Run:
bun run dev- Browser:
/register->/loginhappy path - Browser/API: repeat register for same username should fail
curl -b cookie.txt http://localhost:3000/api/auth/meafter logout should return unauthorized
Expected: all six acceptance points pass exactly as spec defines.
- Step 5: Commit
git add app/pages/register/index.vue
git commit -m "feat: add register page for username-password flow"
Task 6: Final verification and integration commit
Files:
-
Modify: all files from Tasks 1-5 (no new scope)
-
Test: migration + build + manual auth flow
-
Step 1: Run migration
Run: bun run db:migrate
Expected: migrations apply successfully, including sessions table.
- Step 2: Run production build check
Run: bun run build
Expected: build succeeds.
- Step 3: Re-run smoke auth flow
# happy path
curl -i -X POST http://localhost:3000/api/auth/register -H "content-type: application/json" -d '{"username":"final_user","password":"123456"}'
curl -i -c cookie.txt -X POST http://localhost:3000/api/auth/login -H "content-type: application/json" -d '{"username":"final_user","password":"123456"}'
curl -i -b cookie.txt http://localhost:3000/api/auth/me
curl -i -b cookie.txt -X POST http://localhost:3000/api/auth/logout
curl -i -b cookie.txt http://localhost:3000/api/auth/me
Expected: first me success, final me unauthorized.
- Step 4: Create integration commit
git add .
git commit -m "feat: implement username-password auth with cookie sessions"
- Step 5: Record delivered scope
Delivered only:
- register/login/logout/me
- username/password auth
- HttpOnly cookie session
Not delivered:
- forgot password, oauth, captcha, sms, email auth