13 KiB
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
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
import svgCaptcha from 'svg-captcha'
interface CaptchaRecord {
text: string
createdAt: number
}
const captchaStore = new Map<string, CaptchaRecord>()
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
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
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
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
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":"<uuid>","svg":"<svg>...</svg>"}}
- Step 3: Commit
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
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
# 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
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
<script setup lang="ts">
import { z } from 'zod'
const formSchema = z
.object({
username: z.string().min(3, '用户名至少需要3个字符').max(30),
password: z.string().min(8, '密码至少需要8个字符'),
confirmPassword: z.string(),
captchaText: z.string().min(1, '请输入验证码'),
})
.refine((data) => data.password === data.confirmPassword, {
message: '两次输入的密码不一致',
path: ['confirmPassword'],
})
const state = reactive({
username: '',
password: '',
confirmPassword: '',
captchaText: '',
})
const captchaToken = ref('')
const captchaSvg = ref('')
const loading = ref(false)
const toast = useToast()
async function fetchCaptcha() {
const res = await $fetch<{ code: number; data: { token: string; svg: string } }>(
'/api/auth/captcha'
)
captchaToken.value = res.data.token
captchaSvg.value = res.data.svg
}
async function onSubmit() {
loading.value = true
try {
const res = await $fetch<{ code: number; message: string; data: unknown }>(
'/api/auth/register',
{
method: 'POST',
body: {
username: state.username,
password: state.password,
confirmPassword: state.confirmPassword,
captchaToken: captchaToken.value,
captchaText: state.captchaText,
},
}
)
if (res.code !== 0) {
toast.add({ title: res.message, color: 'error' })
if (res.message.includes('验证码')) {
fetchCaptcha()
state.captchaText = ''
}
return
}
navigateTo('/login?registered=1')
} catch {
toast.add({ title: '注册失败,请稍后重试', color: 'error' })
} finally {
loading.value = false
}
}
onMounted(fetchCaptcha)
</script>
<template>
<div class="flex items-center justify-center py-8">
<UCard class="w-full max-w-[380px]">
<template #header>
<h1 class="text-xl font-semibold">Create Account</h1>
<p class="text-sm text-muted mt-1">Fill in the details to get started</p>
</template>
<UForm :state="state" :schema="formSchema" @submit="onSubmit">
<UFormField name="username" label="Username" required class="mb-4">
<UInput v-model="state.username" placeholder="Enter your username" />
</UFormField>
<UFormField name="password" label="Password" required class="mb-4">
<UInput
v-model="state.password"
type="password"
placeholder="At least 8 characters"
/>
</UFormField>
<UFormField name="confirmPassword" label="Confirm Password" required class="mb-4">
<UInput
v-model="state.confirmPassword"
type="password"
placeholder="Re-enter your password"
/>
</UFormField>
<UFormField name="captchaText" label="Captcha" required class="mb-4">
<div class="flex gap-2 items-start">
<!-- eslint-disable-next-line vue/no-v-html -->
<div
class="flex-1 h-10 border rounded-md overflow-hidden bg-[#f8f9fa]"
v-html="captchaSvg"
/>
<UButton
variant="ghost"
color="neutral"
icon="i-lucide-refresh-cw"
square
@click="fetchCaptcha"
/>
</div>
<UInput
v-model="state.captchaText"
placeholder="Enter the code above"
class="mt-2"
/>
</UFormField>
<UButton type="submit" block :loading="loading" class="mt-6">
Create Account
</UButton>
</UForm>
<template #footer>
<p class="text-sm text-center text-muted">
Already have an account?
<NuxtLink to="/login" class="text-primary hover:underline">Log in</NuxtLink>
</p>
</template>
</UCard>
</div>
</template>
- Step 2: Commit
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:
<UButton color="neutral" variant="ghost" label="登录 / 注册" />
Replace with:
<UButton color="neutral" variant="ghost" label="登录 / 注册" to="/register" />
- 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
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
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)
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)
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
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}