Securing Server Actions
Middleware protects pages, but Server Actions need their own authorization checks. A malicious actor could call a Server Action directly via a POST request, bypassing the UI entirely.
Always Authorize in Server Actions
// app/actions/posts.ts
'use server';
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
export async function deletePost(postId: string) {
// Always check the session in every Server Action that mutates data
const session = await auth();
if (!session?.user?.id) {
redirect('/login');
}
const post = await db.post.findUnique({
where: { id: postId },
select: { authorId: true },
});
if (!post) {
throw new Error('Post not found');
}
// Check ownership
if (post.authorId !== session.user.id && session.user.role !== 'admin') {
throw new Error('You do not have permission to delete this post');
}
await db.post.delete({ where: { id: postId } });
revalidatePath('/blog');
}
Helper for Getting the Current User
// lib/auth-helpers.ts
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
export async function requireUser() {
const session = await auth();
if (!session?.user?.id) {
redirect('/login');
}
return session.user;
}
export async function requireAdmin() {
const user = await requireUser();
if (user.role !== 'admin') {
redirect('/unauthorized');
}
return user;
}
Use in actions and page components:
// app/actions/admin.ts
'use server';
import { requireAdmin } from '@/lib/auth-helpers';
export async function banUser(userId: string) {
await requireAdmin(); // Throws/redirects if not admin
await db.user.update({
where: { id: userId },
data: { banned: true },
});
}
Handling Unauthorized Errors in UI
'use client';
import { deletePost } from '@/actions/posts';
export function DeleteButton({ postId }: { postId: string }) {
async function handleDelete() {
try {
await deletePost(postId);
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to delete');
}
}
return (
<button
onClick={handleDelete}
className="text-red-600 hover:text-red-800"
>
Delete
</button>
);
}
Never trust the client to enforce authorization. Always verify the session server-side in every action that reads or modifies sensitive data.