Tech2 views

Part 7: Adding Simple Authentication to blog-demo (Next.js + Prisma + Auth.js)

In the previous article, we already got blog-demo running in Next.js: we can list articles, create articles, view details, and add comments.

But there is an obvious problem: all write operations use the hardcoded authorId = 1 by default, with no login concept at all.

In this article, we will do three things:

  1. Add a minimal usable login system (Auth.js / NextAuth style) to the project;

  2. Change blog-demo’s “New Article / Post a Comment” to “Must be logged in to operate”;

  3. In the API Route, retrieve the current user ID from the session instead of the hardcoded authorId.

We won’t use third-party OAuth, nor a complex registration process. We’ll only use the simplest version of Email/Password or Credentials login to build the overall skeleton of “Prisma + Next.js + Auth”.

Note: This article focuses on “engineering connection”, not on the full functionality of Auth.js, so the configuration will be as concise and clear as possible.


1. Choosing an Auth Solution: Why Auth.js / NextAuth

Next.js’s recommended auth solution is Auth.js (formerly NextAuth). Community examples almost always pair it with Prisma.

The benefits are:

In this article, we’ll use Auth.js as an example, focusing on:


2. Extending the Schema: Adding User Table Structure for Auth.js

Auth.js generally requires a set of models (User / Account / Session / VerificationToken). The official command can add these directly to the Prisma schema.

But we already have our own blog-demo User model and don’t want to be completely overwritten. There are two approaches:

For simplicity, we will choose: unify the User model and let Auth.js also use it.

You can extend the existing User model slightly, e.g., add fields for login (like username or hashed password) and possibly a role field (role control will be used later).

text
model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String
  // For simplicity, use username + hashedPassword
  username  String?   @unique
  password  String?   // Store encrypted password (must be hashed in production)

  role      String    @default("USER") // Simple role

  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  profile   Profile?
  posts     Post[]
  comments  Comment[]

  // If you use Auth.js PrismaAdapter later, you also need to add Account/Session etc. models per its docs
}

If you plan to use the full Auth.js PrismaAdapter, it's best to follow the official guide to have it create all models at once, then adjust blog-demo’s relations to use its User.

To keep focus, we assume:


3. Configuring Auth.js in Next.js: auth.ts + /api/auth

Following the recommended usage for the App Router, we typically configure Auth.js in lib/auth.ts and export the handler in app/api/auth/[...nextauth]/route.ts.

3.1 Install Dependencies

In the project root:

text
pnpm add next-auth @auth/prisma-adapter

3.2 Configure Auth.js: lib/auth.ts

Example lib/auth.ts (simplified, using Credentials Provider):

text
// lib/auth.ts
import NextAuth, { type NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { blogPrisma } from './prisma-blog'
import type { User as PrismaUser } from '../generated/blog'

export const authConfig: NextAuthConfig = {
  adapter: PrismaAdapter(blogPrisma as any),
  session: {
    strategy: 'jwt',
  },
  providers: [
    Credentials({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials.password) return null

        const user = await blogPrisma.user.findUnique({
          where: { email: credentials.email },
        })
        if (!user) return null

        // Password verification should be done here (BCrypt etc.)
        // For simplicity, allow any password for now
        // if (!compareHash(credentials.password, user.password)) return null

        return {
          id: String(user.id),
          name: user.name,
          email: user.email,
          role: user.role,
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = (user as any).role
      }
      return token
    },
    async session({ session, token }) {
      if (token && session.user) {
        session.user.id = token.id as string
        ;(session.user as any).role = token.role
      }
      return session
    },
  },
}

export const { handlers, auth, signIn, signOut } = NextAuth(authConfig)

A few key points:

In production, proper password hashing verification should be used. Here we omit the specific password logic to focus on the overall structure of Prisma combined with auth.

3.3 /api/auth/[...nextauth] Route

app/api/auth/[...nextauth]/route.ts:

text
import { handlers } from '@/lib/auth'

// Auth.js will use GET/POST handlers
export const { GET, POST } = handlers

At this point, the Auth.js foundations are set. You can use the hooks from next-auth/react on the client to login/logout; on the server, use auth() to get current user info.


4. Using auth() in API Routes: Restrict Write Operations to Logged‑in Users

Now, let’s revisit the API Routes we wrote in Part 6, such as /api/posts and /api/posts/[id]/comments.

The goals are:

4.1 Modify POST /api/posts: Use the Currently Logged‑in User as Author

Modify POST in app/api/posts/route.ts:

text
import { NextRequest, NextResponse } from 'next/server'
import { blogPrisma } from '@/lib/prisma-blog'
import { auth } from '@/lib/auth'

export async function POST(req: NextRequest) {
  try {
    const session = await auth()
    if (!session?.user?.id) {
      return NextResponse.json(
        { error: 'Not logged in, cannot create a post' },
        { status: 401 },
      )
    }

    const userId = Number(session.user.id)
    const body = await req.json()
    const { title, content, categorySlugs } = body

    if (!title) {
      return NextResponse.json(
        { error: 'title is required' },
        { status: 400 },
      )
    }

    const post = await blogPrisma.$transaction(async (tx) => {
      const created = await tx.post.create({
        data: {
          title,
          content,
          published: true,
          authorId: userId,
        },
      })

      if (Array.isArray(categorySlugs) && categorySlugs.length > 0) {
        const categories = await tx.category.findMany({
          where: { slug: { in: categorySlugs } },
        })
        if (categories.length > 0) {
          await tx.postCategory.createMany({
            data: categories.map((c) => ({
              postId: created.id,
              categoryId: c.id,
            })),
            skipDuplicates: true,
          })
        }
      }

      return await tx.post.findUnique({
        where: { id: created.id },
        include: {
          author: { select: { id: true, name: true } },
          categories: { include: { category: true } },
        },
      })
    })

    if (!post) {
      return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
    }

    const result = {
      id: post.id,
      title: post.title,
      content: post.content,
      createdAt: post.createdAt,
      author: post.author,
      categories: post.categories.map((pc) => pc.category),
    }

    return NextResponse.json(result, { status: 201 })
  } catch (e: any) {
    console.error(e)
    return NextResponse.json(
      { error: e.message || 'Failed to create post' },
      { status: 500 },
    )
  }
}

Key changes:

4.2 Modify POST /api/posts/[id]/comments: Use the Currently Logged‑in User as Commenter

Similarly, add login protection to the comment endpoint:

app/api/posts/[id]/comments/route.ts:

text
import { NextRequest, NextResponse } from 'next/server'
import { blogPrisma } from '@/lib/prisma-blog'
import { auth } from '@/lib/auth'

type RouteParams = {
  params: { id: string }
}

export async function POST(req: NextRequest, { params }: RouteParams) {
  try {
    const session = await auth()
    if (!session?.user?.id) {
      return NextResponse.json(
        { error: 'Not logged in, cannot comment' },
        { status: 401 },
      )
    }
    const authorId = Number(session.user.id)
    const postId = Number(params.id)
    const body = await req.json()
    const { content } = body

    if (!content || !Number.isFinite(postId)) {
      return NextResponse.json(
        { error: 'content and a valid postId are required' },
        { status: 400 },
      )
    }

    const comment = await blogPrisma.$transaction(async (tx) => {
      const post = await tx.post.findUnique({
        where: { id: postId },
        select: { id: true },
      })
      if (!post) throw new Error('Post does not exist')

      return await tx.comment.create({
        data: {
          content,
          postId,
          authorId,
        },
        include: {
          author: { select: { id: true, name: true } },
        },
      })
    })

    return NextResponse.json(comment, { status: 201 })
  } catch (e: any) {
    console.error(e)
    const message =
      e.message === 'Post does not exist' ? e.message : 'Failed to create comment'
    const status = message === 'Post does not exist' ? 400 : 500
    return NextResponse.json({ error: message }, { status })
  }
}

At this point, blog-demo’s “write” endpoints are tied to login status:


5. Using Login Status in Pages: Show Only Relevant Buttons

Now adjust the frontend pages from the previous article: replace the hardcoded authorId = 1 with login‑dependent logic, and add UI decisions:

5.1 Using useSession in Client Components

If you use next-auth/react, you can use the useSession hook on the client to get the login status.

Install:

text
pnpm add next-auth

Inject SessionProvider in app/layout.tsx (abbreviated), then use it in comment-form.tsx and new/page.tsx.

Example with CommentForm:

text
'use client'

import { FormEvent, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'

export default function CommentForm({ postId }: { postId: number }) {
  const router = useRouter()
  const { data: session, status } = useSession()
  const [content, setContent] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  if (status === 'loading') {
    return <p className="text-xs text-gray-500">Checking login status...</p>
  }

  if (!session?.user) {
    return (
      <p className="text-xs text-gray-500">
        You must be logged in to post a comment.
      </p>
    )
  }

  async function handleSubmit(e: FormEvent) {
    e.preventDefault()
    setError(null)
    setLoading(true)
    try {
      const res = await fetch(`/api/posts/${postId}/comments`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content }),
      })
      const data = await res.json()
      if (!res.ok) {
        throw new Error(data.error || 'Comment failed')
      }
      setContent('')
      router.refresh()
    } catch (err: any) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-2">
      <textarea
        className="w-full rounded border px-3 py-2 text-sm min-h-[80px]"
        placeholder="Write your comment..."
        value={content}
        onChange={(e) => setContent(e.target.value)}
        required
      />
      {error && <p className="text-xs text-red-500">{error}</p>}
      <button
        type="submit"
        disabled={loading}
        className="rounded bg-blue-600 px-3 py-1 text-xs text-white disabled:opacity-60"
      >
        {loading ? 'Submitting...' : 'Post Comment'}
      </button>
    </form>
  )
}

Similarly, the new‑article page can disable the form when not logged in and show a “Please log in” prompt.


6. Summary: Basic Integration of Prisma + Next.js + Auth

This article essentially did two things:

From Prisma’s perspective, almost nothing changed:

SHARE

Share

Share this article.