AgentSkillsCN

Auth

认证

SKILL.md

⚠️ MANDATORY: COPY THIS CODE EXACTLY - DO NOT MODIFY

This skill implements NextAuth.js (Auth.js) v5 with Next.js App Router.

Step 1: Install dependencies

```bash npm install next-auth@beta @auth/drizzle-adapter drizzle-orm ```

Step 2: Auth configuration (copy exactly)

```typescript // lib/auth.ts import NextAuth from 'next-auth'; import Google from 'next-auth/providers/google'; import GitHub from 'next-auth/providers/github'; import Credentials from 'next-auth/providers/credentials'; import { DrizzleAdapter } from '@auth/drizzle-adapter'; import { db } from '@/lib/db'; import bcrypt from 'bcryptjs';

export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: DrizzleAdapter(db), providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), Credentials({ name: 'credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null;

code
    // Replace with your user lookup logic
    const user = await db.query.users.findFirst({
      where: (users, { eq }) => eq(users.email, credentials.email as string),
    });
    
    if (!user || !user.password) return null;
    
    const isValid = await bcrypt.compare(credentials.password as string, user.password);
    if (!isValid) return null;
    
    return { id: user.id, name: user.name, email: user.email };
  },
}),

], session: { strategy: 'jwt' }, pages: { signIn: '/login', error: '/login', }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; } return token; }, async session({ session, token }) { if (session.user) { session.user.id = token.id as string; } return session; }, }, }); ```

Step 3: API route handler (copy exactly)

```typescript // app/api/auth/[...nextauth]/route.ts import { handlers } from '@/lib/auth'; export const { GET, POST } = handlers; ```

Step 4: Middleware for protected routes (copy exactly)

```typescript // middleware.ts import { auth } from '@/lib/auth'; import { NextResponse } from 'next/server';

export default auth((req) => { const isLoggedIn = !!req.auth; const isAuthPage = req.nextUrl.pathname.startsWith('/login') || req.nextUrl.pathname.startsWith('/register'); const isProtectedRoute = req.nextUrl.pathname.startsWith('/dashboard') || req.nextUrl.pathname.startsWith('/settings');

// Redirect logged-in users away from auth pages if (isLoggedIn && isAuthPage) { return NextResponse.redirect(new URL('/dashboard', req.nextUrl)); }

// Redirect unauthenticated users to login if (!isLoggedIn && isProtectedRoute) { return NextResponse.redirect(new URL('/login', req.nextUrl)); }

return NextResponse.next(); });

export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], }; ```

Step 5: Session provider (copy exactly)

```tsx // app/providers.tsx 'use client'; import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) { return <SessionProvider>{children}</SessionProvider>; } ```

Step 6: Wrap app in providers

```tsx // app/layout.tsx import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); } ```

Step 7: Login page (copy exactly)

```tsx // app/login/page.tsx 'use client'; import { signIn } from 'next-auth/react'; import { useState } from 'react'; import { useRouter } from 'next/navigation';

export default function LoginPage() { const router = useRouter(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false);

const handleCredentialsLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError('');

code
const result = await signIn('credentials', {
  email,
  password,
  redirect: false,
});

if (result?.error) {
  setError('Invalid email or password');
  setLoading(false);
} else {
  router.push('/dashboard');
}

};

return ( <div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg"> <div className="text-center"> <h2 className="text-3xl font-bold">Sign in</h2> <p className="mt-2 text-gray-600">Welcome back</p> </div>

code
    {/* OAuth Buttons */}
    <div className="space-y-3">
      <button
        onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
        className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
      >
        <svg className="w-5 h-5" viewBox="0 0 24 24">
          <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
          <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
          <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
          <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
        </svg>
        Continue with Google
      </button>

      <button
        onClick={() => signIn('github', { callbackUrl: '/dashboard' })}
        className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
      >
        <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
          <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
        </svg>
        Continue with GitHub
      </button>
    </div>

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

    {/* Credentials Form */}
    <form onSubmit={handleCredentialsLogin} className="space-y-4">
      {error && (
        <div className="p-3 text-sm text-red-500 bg-red-50 rounded-lg">
          {error}
        </div>
      )}
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"
        />
      </div>
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"
        />
      </div>
      <button
        type="submit"
        disabled={loading}
        className="w-full py-3 px-4 bg-black text-white rounded-lg hover:bg-gray-800 transition disabled:opacity-50"
      >
        {loading ? 'Signing in...' : 'Sign in'}
      </button>
    </form>

    <p className="text-center text-sm text-gray-600">
      Don't have an account?{' '}
      <a href="/register" className="font-medium text-black hover:underline">
        Sign up
      </a>
    </p>
  </div>
</div>

); } ```

Step 8: Registration page (copy exactly)

```tsx // app/register/page.tsx 'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation';

export default function RegisterPage() { const router = useRouter(); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false);

const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError('');

code
try {
  const res = await fetch('/api/auth/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, email, password }),
  });

  if (!res.ok) {
    const data = await res.json();
    throw new Error(data.error || 'Registration failed');
  }

  router.push('/login?registered=true');
} catch (err: any) {
  setError(err.message);
  setLoading(false);
}

};

return ( <div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg"> <div className="text-center"> <h2 className="text-3xl font-bold">Create account</h2> <p className="mt-2 text-gray-600">Get started for free</p> </div>

code
    <form onSubmit={handleRegister} className="space-y-4">
      {error && (
        <div className="p-3 text-sm text-red-500 bg-red-50 rounded-lg">
          {error}
        </div>
      )}
      <div>
        <label htmlFor="name" className="block text-sm font-medium text-gray-700">
          Name
        </label>
        <input
          id="name"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"
        />
      </div>
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"
        />
      </div>
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          Password
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          minLength={8}
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"
        />
      </div>
      <button
        type="submit"
        disabled={loading}
        className="w-full py-3 px-4 bg-black text-white rounded-lg hover:bg-gray-800 transition disabled:opacity-50"
      >
        {loading ? 'Creating account...' : 'Create account'}
      </button>
    </form>

    <p className="text-center text-sm text-gray-600">
      Already have an account?{' '}
      <a href="/login" className="font-medium text-black hover:underline">
        Sign in
      </a>
    </p>
  </div>
</div>

); } ```

Step 9: Registration API route (copy exactly)

```typescript // app/api/auth/register/route.ts import { NextResponse } from 'next/server'; import bcrypt from 'bcryptjs'; import { db } from '@/lib/db'; import { users } from '@/lib/db/schema';

export async function POST(req: Request) { try { const { name, email, password } = await req.json();

code
if (!name || !email || !password) {
  return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
}

if (password.length < 8) {
  return NextResponse.json({ error: 'Password must be at least 8 characters' }, { status: 400 });
}

// Check if user exists
const existing = await db.query.users.findFirst({
  where: (users, { eq }) => eq(users.email, email),
});

if (existing) {
  return NextResponse.json({ error: 'Email already registered' }, { status: 400 });
}

// Hash password
const hashedPassword = await bcrypt.hash(password, 12);

// Create user
await db.insert(users).values({
  id: crypto.randomUUID(),
  name,
  email,
  password: hashedPassword,
});

return NextResponse.json({ success: true });

} catch (error) { console.error('Registration error:', error); return NextResponse.json({ error: 'Registration failed' }, { status: 500 }); } } ```

Step 10: User profile component (copy exactly)

```tsx // components/user-profile.tsx 'use client'; import { useSession, signOut } from 'next-auth/react'; import Image from 'next/image';

export function UserProfile() { const { data: session, status } = useSession();

if (status === 'loading') { return <div className="animate-pulse h-10 w-10 bg-gray-200 rounded-full" />; }

if (!session?.user) { return ( <a href="/login" className="px-4 py-2 bg-black text-white rounded-lg hover:bg-gray-800"> Sign in </a> ); }

return ( <div className="flex items-center gap-3"> {session.user.image ? ( <Image src={session.user.image} alt={session.user.name || 'User'} width={40} height={40} className="rounded-full" /> ) : ( <div className="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center"> {session.user.name?.[0] || session.user.email?.[0] || '?'} </div> )} <div className="hidden sm:block"> <p className="text-sm font-medium">{session.user.name}</p> <p className="text-xs text-gray-500">{session.user.email}</p> </div> <button onClick={() => signOut({ callbackUrl: '/' })} className="px-3 py-1.5 text-sm text-gray-600 hover:text-black" > Sign out </button> </div> ); } ```

Step 11: Server-side session access (copy exactly)

```tsx // app/dashboard/page.tsx import { auth } from '@/lib/auth'; import { redirect } from 'next/navigation';

export default async function DashboardPage() { const session = await auth();

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

return ( <div className="min-h-screen p-8"> <h1 className="text-2xl font-bold mb-4">Dashboard</h1> <p className="text-gray-600">Welcome back, {session.user.name || session.user.email}!</p>

code
  <div className="mt-8 p-6 bg-white rounded-xl shadow">
    <h2 className="font-semibold mb-4">Your Profile</h2>
    <dl className="space-y-2">
      <div>
        <dt className="text-sm text-gray-500">Name</dt>
        <dd>{session.user.name || 'Not set'}</dd>
      </div>
      <div>
        <dt className="text-sm text-gray-500">Email</dt>
        <dd>{session.user.email}</dd>
      </div>
      <div>
        <dt className="text-sm text-gray-500">User ID</dt>
        <dd className="font-mono text-sm">{session.user.id}</dd>
      </div>
    </dl>
  </div>
</div>

); } ```

Step 12: Database schema for users (copy exactly)

```typescript // lib/db/schema.ts import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', { id: text('id').primaryKey(), name: text('name'), email: text('email').unique().notNull(), emailVerified: integer('email_verified', { mode: 'timestamp' }), image: text('image'), password: text('password'), // For credentials provider createdAt: integer('created_at', { mode: 'timestamp' }).default(Date.now()), });

export const accounts = sqliteTable('accounts', { id: text('id').primaryKey(), userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), type: text('type').notNull(), provider: text('provider').notNull(), providerAccountId: text('provider_account_id').notNull(), refresh_token: text('refresh_token'), access_token: text('access_token'), expires_at: integer('expires_at'), token_type: text('token_type'), scope: text('scope'), id_token: text('id_token'), session_state: text('session_state'), });

export const sessions = sqliteTable('sessions', { id: text('id').primaryKey(), sessionToken: text('session_token').unique().notNull(), userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), expires: integer('expires', { mode: 'timestamp' }).notNull(), }); ```

Environment variables needed:

  • AUTH_SECRET (run: openssl rand -base64 32)
  • GOOGLE_CLIENT_ID (from Google Cloud Console)
  • GOOGLE_CLIENT_SECRET
  • GITHUB_CLIENT_ID (from GitHub Developer Settings)
  • GITHUB_CLIENT_SECRET

❌ DO NOT:

  • Never store plain text passwords
  • Never expose AUTH_SECRET to client
  • Never skip the middleware for protected routes
  • Never trust client-side auth checks alone

✅ DO:

  • Always hash passwords with bcrypt (12+ rounds)
  • Always use middleware for route protection
  • Always validate session server-side for sensitive operations
  • Use secure, httpOnly cookies (NextAuth handles this)
  • Implement rate limiting on login attempts