Claude
Skills
Sign in
Back

auth-dev

Included with Lifetime
$97 forever

Dev-mode authentication tooling — seed dev users and quick sign-in buttons for each role. Use this skill when the user says "setup dev auth", "add dev login", "setup dev users", "seed test users", or "dev sign-in".

Code Review

What this skill does


# Dev Authentication Tooling

Adds one-click dev sign-in buttons directly on the login screen, a development-only `/dev` console page, and an API endpoint that seeds dev users into the database. Users are auto-seeded when the login page loads. Everything is gated behind `NODE_ENV !== 'production'` so none of this code runs in production builds.

## What Gets Created

1. **`src/app/api/dev/seed/route.ts`** — Seeds dev users via better-auth internal API + Drizzle role patch
2. **`src/components/auth/dev-login-panel.tsx`** — One-click sign-in buttons shown below the login form (auto-seeds on mount)
3. **`src/app/(auth)/sign-in/page.tsx`** — Updated to include DevLoginPanel in development
4. **`src/app/dev/layout.tsx`** — Dev-only layout guard (returns 404 in production)
5. **`src/app/dev/page.tsx`** — Dev console with seed button + quick sign-in cards

## Prerequisites

- `auth` skill fully applied (auth config, database schema, auth client)
- Database running and schema pushed (`bunx drizzle-kit push`)
- Dev server running (`bun run dev`)

## Dev Users

| Role   | Email              | Password   |
|--------|--------------------|------------|
| admin  | <[email protected]>  | Admin123!  |
| member | <[email protected]> | Member123! |

## Setup Steps

### 1. Create Dev Layout Guard (`src/app/dev/layout.tsx`)

```tsx
import { notFound } from "next/navigation";

export default function DevLayout({ children }: { children: React.ReactNode }) {
  if (process.env.NODE_ENV === "production") {
    notFound();
  }

  return (
    <div className="min-h-screen bg-zinc-950 text-zinc-100">
      <div className="mx-auto max-w-xl px-4 py-12">
        <div className="mb-8 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-400">
          Development only — this page is not available in production.
        </div>
        {children}
      </div>
    </div>
  );
}
```

### 2. Create Dev Console Page (`src/app/dev/page.tsx`)

```tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn, signOut, useSession } from "@/lib/auth-client";

const DEV_USERS = [
  { email: "[email protected]", password: "Admin123!", name: "Admin User", role: "admin" },
  { email: "[email protected]", password: "Member123!", name: "Member User", role: "member" },
] as const;

export default function DevPage() {
  const router = useRouter();
  const { data: session, isPending } = useSession();
  const [seeding, setSeeding] = useState(false);
  const [seedResult, setSeedResult] = useState<string | null>(null);
  const [signingIn, setSigningIn] = useState<string | null>(null);

  const handleSeed = async () => {
    setSeeding(true);
    setSeedResult(null);
    try {
      const res = await fetch("/api/dev/seed", { method: "POST" });
      const data = await res.json();
      if (res.ok) {
        setSeedResult(`Seeded ${data.results.length} users: ${data.results.map((r: { email: string; status: string }) => `${r.email} (${r.status})`).join(", ")}`);
      } else {
        setSeedResult(`Error: ${data.error}`);
      }
    } catch (err) {
      setSeedResult(`Network error: ${err instanceof Error ? err.message : "unknown"}`);
    } finally {
      setSeeding(false);
    }
  };

  const handleSignIn = async (email: string, password: string) => {
    setSigningIn(email);
    try {
      const result = await signIn.email({ email, password });
      if (result.error) {
        setSeedResult(`Sign-in failed for ${email}: ${result.error.message ?? "unknown error"} — try seeding first.`);
      } else {
        router.push("/");
        router.refresh();
      }
    } finally {
      setSigningIn(null);
    }
  };

  const handleSignOut = async () => {
    await signOut();
    router.refresh();
  };

  return (
    <div className="space-y-8">
      <div>
        <h1 className="text-2xl font-bold">Dev Console</h1>
        <p className="mt-1 text-sm text-zinc-400">Quick authentication for development</p>
      </div>

      {/* Current session */}
      <div className="rounded-xl border border-zinc-800 bg-zinc-900 p-5">
        <h2 className="mb-3 text-sm font-medium uppercase tracking-wider text-zinc-500">Current Session</h2>
        {isPending ? (
          <div className="h-5 w-32 animate-pulse rounded bg-zinc-800" />
        ) : session ? (
          <div className="flex items-center justify-between">
            <div>
              <p className="font-medium">{session.user.name}</p>
              <p className="text-sm text-zinc-400">{session.user.email}</p>
            </div>
            <button
              type="button"
              onClick={handleSignOut}
              className="rounded-lg border border-zinc-700 px-3 py-1.5 text-sm hover:bg-zinc-800"
            >
              Sign Out
            </button>
          </div>
        ) : (
          <p className="text-sm text-zinc-500">Not signed in</p>
        )}
      </div>

      {/* Seed */}
      <div className="rounded-xl border border-zinc-800 bg-zinc-900 p-5">
        <div className="flex items-center justify-between">
          <div>
            <h2 className="text-sm font-medium uppercase tracking-wider text-zinc-500">Database Seed</h2>
            <p className="mt-1 text-sm text-zinc-400">Create dev users in the database</p>
          </div>
          <button
            type="button"
            onClick={handleSeed}
            disabled={seeding}
            className="rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500 disabled:opacity-50"
          >
            {seeding ? "Seeding..." : "Seed Users"}
          </button>
        </div>
        {seedResult && (
          <p className="mt-3 rounded-lg bg-zinc-800 px-3 py-2 text-sm text-zinc-300">{seedResult}</p>
        )}
      </div>

      {/* Quick sign-in cards */}
      <div>
        <h2 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500">Quick Sign In</h2>
        <div className="grid gap-4">
          {DEV_USERS.map((devUser) => (
            <div
              key={devUser.email}
              className="flex items-center justify-between rounded-xl border border-zinc-800 bg-zinc-900 p-5"
            >
              <div>
                <p className="font-medium">{devUser.name}</p>
                <p className="text-sm text-zinc-400">{devUser.email}</p>
                <span className="mt-1 inline-block rounded-full bg-zinc-800 px-2.5 py-0.5 text-xs text-zinc-300">
                  {devUser.role}
                </span>
              </div>
              <button
                type="button"
                onClick={() => handleSignIn(devUser.email, devUser.password)}
                disabled={signingIn === devUser.email}
                className="rounded-lg bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-900 hover:bg-white disabled:opacity-50"
              >
                {signingIn === devUser.email ? "Signing in..." : "Sign In"}
              </button>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}
```

### 3. Create Seed API Route (`src/app/api/dev/seed/route.ts`)

> Uses better-auth's internal `signUpEmail` API so password hashing is handled automatically — no need for manual scrypt. Falls back gracefully if users already exist.

```typescript
import { NextResponse } from "next/server";

const DEV_USERS = [
  { email: "[email protected]", password: "Admin123!", name: "Admin User", role: "admin" },
  { email: "[email protected]", password: "Member123!", name: "Member User", role: "member" },
] as const;

export async function POST() {
  if (process.env.NODE_ENV === "production") {
    return NextResponse.json({ error: "Not available in production" }, { status: 404 });
  }

  const { auth } = await import("@/lib/auth");
  const { db } = await import("@/lib/db");
  const { user } = await import("@/lib/db/schema/auth");
  const { eq } = await import("drizzle-orm");

  const results: { email:
Files: 1
Size: 15.8 KB
Complexity: 25/100
Category: Code Review

Related in Code Review