You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

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}