AgentSkillsCN

Auth Scaffold

身份验证脚手架

SKILL.md

Auth Scaffold Skill

Generate Complete Auth System: Login, register, forgot password, and session management.


🎯 Purpose

When user says: "Add authentication" or "Create login/signup"

Generate a complete authentication system with:

  • Login page
  • Register page
  • Forgot password
  • Session management
  • Protected routes

📁 Generated Structure

code
src/
├── app/
│   ├── (auth)/
│   │   ├── layout.tsx
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── register/
│   │   │   └── page.tsx
│   │   └── forgot-password/
│   │       └── page.tsx
│   └── (protected)/
│       └── layout.tsx
├── components/
│   └── auth/
│       ├── login-form.tsx
│       ├── register-form.tsx
│       ├── forgot-password-form.tsx
│       ├── user-button.tsx
│       └── auth-provider.tsx
├── lib/
│   ├── auth.ts
│   └── validations/
│       └── auth.ts
└── auth.ts (NextAuth config)

🔐 NextAuth Configuration

typescript
// auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { db } from "@/lib/db"
import bcrypt from "bcryptjs"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(db),
  session: { strategy: "jwt" },
  pages: {
    signIn: "/login",
    error: "/login",
  },
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    Credentials({
      credentials: {
        email: { type: "email" },
        password: { type: "password" },
      },
      authorize: async (credentials) => {
        const { email, password } = credentials

        const user = await db.user.findUnique({
          where: { email: email as string },
        })

        if (!user || !user.passwordHash) return null

        const isValid = await bcrypt.compare(
          password as string,
          user.passwordHash
        )

        if (!isValid) return null

        return { id: user.id, email: user.email, name: user.name }
      },
    }),
  ],
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) token.id = user.id
      return token
    },
    session: async ({ session, token }) => {
      if (session.user) session.user.id = token.id as string
      return session
    },
  },
})

📝 Login Form

tsx
// components/auth/login-form.tsx
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { signIn } from "next-auth/react"
import { useRouter, useSearchParams } from "next/navigation"
import { useState } from "react"

const loginSchema = z.object({
  email: z.string().email("Invalid email"),
  password: z.string().min(8, "Password must be at least 8 characters"),
})

export function LoginForm() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const [error, setError] = useState("")

  const form = useForm({
    resolver: zodResolver(loginSchema),
    defaultValues: { email: "", password: "" },
  })

  const onSubmit = async (data: z.infer<typeof loginSchema>) => {
    setError("")
    
    const result = await signIn("credentials", {
      email: data.email,
      password: data.password,
      redirect: false,
    })

    if (result?.error) {
      setError("Invalid email or password")
      return
    }

    router.push(searchParams.get("callbackUrl") || "/dashboard")
    router.refresh()
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
      {error && (
        <div className="p-3 bg-red-100 text-red-600 rounded">{error}</div>
      )}

      <div>
        <label>Email</label>
        <input type="email" {...form.register("email")} className="w-full p-2 border rounded" />
        {form.formState.errors.email && (
          <p className="text-red-500 text-sm">{form.formState.errors.email.message}</p>
        )}
      </div>

      <div>
        <label>Password</label>
        <input type="password" {...form.register("password")} className="w-full p-2 border rounded" />
        {form.formState.errors.password && (
          <p className="text-red-500 text-sm">{form.formState.errors.password.message}</p>
        )}
      </div>

      <button type="submit" className="w-full py-2 bg-blue-600 text-white rounded">
        Sign In
      </button>

      <div className="relative my-4">
        <div className="absolute inset-0 flex items-center">
          <div className="w-full border-t" />
        </div>
        <div className="relative flex justify-center text-sm">
          <span className="bg-white px-2 text-gray-500">Or continue with</span>
        </div>
      </div>

      <button
        type="button"
        onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
        className="w-full py-2 border rounded flex items-center justify-center gap-2"
      >
        <GoogleIcon /> Google
      </button>
    </form>
  )
}

📝 Register Form

tsx
// components/auth/register-form.tsx
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { useRouter } from "next/navigation"
import { useState } from "react"

const registerSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
})

export function RegisterForm() {
  const router = useRouter()
  const [error, setError] = useState("")

  const form = useForm({
    resolver: zodResolver(registerSchema),
  })

  const onSubmit = async (data: z.infer<typeof registerSchema>) => {
    const res = await fetch("/api/auth/register", {
      method: "POST",
      body: JSON.stringify(data),
    })

    if (!res.ok) {
      const error = await res.json()
      setError(error.message)
      return
    }

    router.push("/login?registered=true")
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
      {/* Form fields similar to login */}
      <button type="submit" className="w-full py-2 bg-blue-600 text-white rounded">
        Create Account
      </button>
    </form>
  )
}

🔒 Protected Layout

tsx
// app/(protected)/layout.tsx
import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function ProtectedLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await auth()

  if (!session?.user) {
    redirect("/login")
  }

  return <>{children}</>
}

📋 Scaffold Checklist

  • NextAuth config created
  • Prisma User schema updated
  • Login page created
  • Register page created
  • Forgot password page created
  • Protected layout created
  • Auth forms created
  • Environment variables added
  • Migration run

Auth ready in minutes!