diff --git a/docs/superpowers/plans/2026-05-15-registration-page.md b/docs/superpowers/plans/2026-05-15-registration-page.md new file mode 100644 index 0000000..a5385df --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-registration-page.md @@ -0,0 +1,469 @@ +# Registration Page 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:** Add a registration page at `/register` with username, password, confirm password, and SVG captcha. On success, redirect to `/login?registered=1`. + +**Architecture:** Server-side captcha generation via `svg-captcha` stored in an in-memory Map with 5-min TTL. Registration API validates input with Zod, verifies captcha, hashes password with bcryptjs, and inserts into the existing SQLite `users` table. Client-side uses Nuxt UI `UForm` with Zod schema validation and `useToast` for error feedback. + +**Tech Stack:** Nuxt 4.4, Nuxt UI 4.6, svg-captcha, bcryptjs (already installed), zod (already installed), Drizzle ORM + SQLite + +--- + +### Task 1: Install svg-captcha + +**Files:** +- Modify: `package.json` + +- [ ] **Step 1: Install the dependency** + +Run: `cd /home/dash/coding/nuxt-app && bun add svg-captcha` + +- [ ] **Step 2: Commit** + +```bash +git add package.json bun.lock +git commit -m "chore: add svg-captcha dependency" +``` + +--- + +### Task 2: Create captcha utility module + +**Files:** +- Create: `server/utils/auth/captcha.ts` + +- [ ] **Step 1: Create the captcha utility** + +```ts +import svgCaptcha from 'svg-captcha' + +interface CaptchaRecord { + text: string + createdAt: number +} + +const captchaStore = new Map() +const CAPTCHA_TTL = 5 * 60 * 1000 + +function cleanupExpired(): void { + const now = Date.now() + for (const [token, record] of captchaStore) { + if (now - record.createdAt > CAPTCHA_TTL) { + captchaStore.delete(token) + } + } +} + +export function generateCaptcha(): { token: string; svg: string } { + cleanupExpired() + const { text, data: svg } = svgCaptcha.create({ + noise: 3, + color: true, + background: '#f8f9fa', + }) + const token = crypto.randomUUID() + captchaStore.set(token, { text, createdAt: Date.now() }) + return { token, svg } +} + +export function verifyCaptcha(token: string, text: string): boolean { + const record = captchaStore.get(token) + if (!record) return false + if (Date.now() - record.createdAt > CAPTCHA_TTL) { + captchaStore.delete(token) + return false + } + const match = record.text.toLowerCase() === text.toLowerCase() + if (match) { + captchaStore.delete(token) + } + return match +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add server/utils/auth/captcha.ts +git commit -m "feat: add captcha generation and verification utility" +``` + +--- + +### Task 3: Create registration validation schema + +**Files:** +- Create: `server/utils/auth/validation.ts` + +- [ ] **Step 1: Create the validation module** + +```ts +import { z } from 'zod' + +export const registerSchema = z + .object({ + username: z + .string() + .min(3, '用户名至少需要3个字符') + .max(30, '用户名最多30个字符'), + password: z.string().min(8, '密码至少需要8个字符'), + confirmPassword: z.string(), + captchaToken: z.string().min(1, '验证码令牌不能为空'), + captchaText: z.string().min(1, '验证码不能为空'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: '两次输入的密码不一致', + path: ['confirmPassword'], + }) +``` + +- [ ] **Step 2: Commit** + +```bash +git add server/utils/auth/validation.ts +git commit -m "feat: add registration Zod validation schema" +``` + +--- + +### Task 4: Create captcha API endpoint + +**Files:** +- Create: `server/api/auth/captcha.get.ts` + +- [ ] **Step 1: Create the captcha GET endpoint** + +```ts +import { generateCaptcha } from '../../utils/auth/captcha' + +export default defineWrappedResponseHandler(async () => { + const { token, svg } = generateCaptcha() + return R.success({ token, svg }) +}) +``` + +- [ ] **Step 2: Verify the endpoint works** + +Run the dev server: `cd /home/dash/coding/nuxt-app && bun run dev` + +Test: `curl -s http://localhost:3000/api/auth/captcha | head -c 200` + +Expected: JSON response with `{"code":0,"data":{"token":"","svg":"..."}}` + +- [ ] **Step 3: Commit** + +```bash +git add server/api/auth/captcha.get.ts +git commit -m "feat: add GET /api/auth/captcha endpoint" +``` + +--- + +### Task 5: Create registration API endpoint + +**Files:** +- Create: `server/api/auth/register.post.ts` + +- [ ] **Step 1: Create the register POST endpoint** + +```ts +import { registerSchema } from '../../utils/auth/validation' +import { verifyCaptcha } from '../../utils/auth/captcha' +import { dbGlobal } from 'drizzle-pkg/lib/db' +import { users } from 'drizzle-pkg/lib/schema/auth' +import { eq } from 'drizzle-orm' +import { hash } from 'bcryptjs' + +export default defineWrappedResponseHandler(async (event) => { + const body = await readBody(event) + + const parsed = registerSchema.safeParse(body) + if (!parsed.success) { + return R.error(parsed.error.errors[0]?.message || '表单验证失败', null) + } + + const { username, password, captchaToken, captchaText } = parsed.data + + if (!verifyCaptcha(captchaToken, captchaText)) { + return R.error('验证码错误或已过期', null) + } + + const existing = await dbGlobal + .select() + .from(users) + .where(eq(users.username, username)) + if (existing.length > 0) { + return R.error('用户名已存在', null) + } + + const hashedPassword = await hash(password, 10) + + const result = await dbGlobal + .insert(users) + .values({ + username, + password: hashedPassword, + role: 'user', + status: 'active', + }) + .returning({ id: users.id }) + + return R.success({ id: result[0].id, username }) +}) +``` + +- [ ] **Step 2: Test registration with curl** + +```bash +# First get a captcha +CAPTCHA=$(curl -s http://localhost:3000/api/auth/captcha) +TOKEN=$(echo $CAPTCHA | jq -r '.data.token') +# The captcha text is human-readable from the SVG — you'll need to decode it manually +# For an automated test, just verify validation works: +curl -s -X POST http://localhost:3000/api/auth/register \ + -H 'Content-Type: application/json' \ + -d "{\"username\":\"ab\",\"password\":\"12345\",\"confirmPassword\":\"12345\",\"captchaToken\":\"\",\"captchaText\":\"\"}" +# Expected: {"code":1,"message":"用户名至少需要3个字符","data":null} +``` + +- [ ] **Step 3: Commit** + +```bash +git add server/api/auth/register.post.ts +git commit -m "feat: add POST /api/auth/register endpoint" +``` + +--- + +### Task 6: Create registration page component + +**Files:** +- Create: `app/pages/register.vue` + +- [ ] **Step 1: Create the registration page** + +```vue + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add app/pages/register.vue +git commit -m "feat: add registration page with captcha support" +``` + +--- + +### Task 7: Wire header button to /register + +**Files:** +- Modify: `app/layouts/default.vue:15` + +- [ ] **Step 1: Make the login/register button a link** + +Find the line: +```html + +``` + +Replace with: +```html + +``` + +- [ ] **Step 2: Stop dev server (if running) and run type check** + +Run: `cd /home/dash/coding/nuxt-app && bunx nuxi typecheck 2>&1 | tail -5` +Expected: No new type errors. + +- [ ] **Step 3: Commit** + +```bash +git add app/layouts/default.vue +git commit -m "feat: wire header login button to /register" +``` + +--- + +### Task 8: End-to-end verification + +- [ ] **Step 1: Start dev server** + +```bash +cd /home/dash/coding/nuxt-app && bun run dev & +``` + +- [ ] **Step 2: Verify the page loads** + +Run: `curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/register` +Expected: `200` + +- [ ] **Step 3: Verify captcha endpoint** + +Run: `curl -s http://localhost:3000/api/auth/captcha | python3 -m json.tool | head -10` +Expected: Valid JSON with `code: 0`, `data.token`, `data.svg` + +- [ ] **Step 4: Verify registration validation (short username)** + +```bash +curl -s -X POST http://localhost:3000/api/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"username":"ab","password":"12345678","confirmPassword":"12345678","captchaToken":"dummy","captchaText":"dummy"}' +``` +Expected: `{"code":1,"message":"用户名至少需要3个字符","data":null}` + +- [ ] **Step 5: Verify registration validation (password mismatch)** + +```bash +curl -s -X POST http://localhost:3000/api/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"username":"testuser","password":"12345678","confirmPassword":"different","captchaToken":"dummy","captchaText":"dummy"}' +``` +Expected: `{"code":1,"message":"两次输入的密码不一致","data":null}` + +- [ ] **Step 6: Verify invalid captcha returns error** + +```bash +curl -s -X POST http://localhost:3000/api/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"username":"testuser","password":"12345678","confirmPassword":"12345678","captchaToken":"invalid-token","captchaText":"abc"}' +``` +Expected: `{"code":1,"message":"验证码错误或已过期","data":null}`