Skip to content

Voters (Authorization)

Voters control who can access which routes.

json
{
  "name": "ArticleVoter",
  "rules": [
    {
      "methods": ["GET"],
      "resource": "Article"
    },
    {
      "methods": ["POST", "PUT", "DELETE"],
      "resource": "Article",
      "condition": {
        "self": true
      }
    }
  ]
}

This means:

  • Anyone can GET articles
  • Only the article's owner can POST, PUT, or DELETE

Rule fields

FieldTypeDescription
methodsHttpMethod[]Allowed HTTP methods
resourcestringModel name (resource being accessed)
condition.selfbooleanUser must be the owner
condition.fieldstringField to compare
condition.operatorstringComparison operator (==, !=, …)
condition.valueanyValue to compare against

Using a voter on a route

Add "voter": "ArticleVoter" (and optionally "auth": true) to a route definition:

json
{
  "method": "DELETE",
  "path": "/api/articles/:id",
  "auth": true,
  "voter": "ArticleVoter"
}

Generated output

src/lib/auth.ts

ts
// Auto-generated by Codabra — do not edit manually
import { NextRequest } from 'next/server';

export function getAuthUser(req: NextRequest): string | null {
  const auth = req.headers.get('authorization');
  if (!auth || !auth.startsWith('Bearer ')) return null;
  return auth.slice(7) || null;
}

src/lib/voters/ArticleVoter.ts

ts
// Auto-generated by Codabra — do not edit manually
import { NextRequest } from 'next/server';

export function checkArticleVoter(
  _req: NextRequest,
  method: string,
  resource: unknown,
  userId: string | null,
): boolean {
  if (['GET'].includes(method as never)) {
    return true;
  }
  if (['POST', 'PUT', 'DELETE'].includes(method as never)) {
    return (
      resource != null &&
      (resource as Record<string, unknown>)['userId'] === userId
    );
  }
  return false;
}

Route handler (excerpt)

Drizzle

ts
export async function DELETE(
  _req: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const userId = getAuthUser(_req);
  if (!userId)
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  const resource = await db
    .select()
    .from(articles)
    .where(eq(articles.id, id))
    .then((r) => r[0] ?? null);
  if (!checkArticleVoter(_req, 'DELETE', resource, userId)) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }
  await db.delete(articles).where(eq(articles.id, id));
  return NextResponse.json({ success: true });
}

Prisma

ts
export async function DELETE(
  _req: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const userId = getAuthUser(_req);
  if (!userId)
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  const resource = await prisma.article.findUnique({ where: { id } });
  if (!checkArticleVoter(_req, 'DELETE', resource, userId)) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }
  const data = await prisma.article.delete({ where: { id } });
  return NextResponse.json({ success: true });
}

Released under the Elastic License 2.0.