withOrganizationAuthRequired

Secure API routes with organization authentication and role-based access control

withOrganizationAuthRequired 🔒

Protect your API routes with organization-level authentication and role-based access control! ✨

What It Does 🎯

The withOrganizationAuthRequired wrapper:

  • 🔐 Verifies user is authenticated
  • 🏢 Checks user belongs to the organization
  • 👮 Enforces minimum role requirements
  • 📦 Provides organization data in context
  • ⚡ Returns 401/403 errors automatically

Server-Side Only

This wrapper is for API routes only. For client components, use the useOrganization hook!

Basic Usage 📝

Without Role Requirement

Any organization member can access:

// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'

export const GET = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    
    // Fetch projects for this organization
    const projects = await getProjects(organization.id)
    
    return NextResponse.json(projects)
  }
)

With Role Requirement

Only admins and owners can access:

// src/app/api/app/org/[slug]/settings/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'

export const PATCH = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const body = await req.json()
    
    // Update organization settings
    await updateOrganizationSettings(organization.id, body)
    
    return NextResponse.json({ success: true })
  },
  OrganizationRole.enum.admin // Requires admin or owner
)

Context Object 📦

The wrapper provides a context object with:

{
  session: {
    expires: string
    user: Promise<{
      id: string
      email: string
      name: string
      image?: string
      // ... other user fields
    }>
    organization: Promise<{
      id: string
      name: string
      slug: string
      image?: string
      role: 'owner' | 'admin' | 'user'
      plan: {
        id: string
        name: string
        codename: string
        default: boolean
        quotas: object
        requiredCouponCount: number
      } | null
    }>
  }
  params: Promise<{
    slug?: string
    // ... other route params
  }>
}

User and Organization are Promises

The user and organization properties are async getters (Promises). You must await them before accessing their properties!

Common Patterns 🚀

GET - Fetch Organization Data

// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { projects } from '@/db/schema'
import { eq } from 'drizzle-orm'

export const GET = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    
    // Fetch projects for this organization
    const orgProjects = await db
      .select()
      .from(projects)
      .where(eq(projects.organizationId, organization.id))
    
    return NextResponse.json(orgProjects)
  }
)

POST - Create Resource

// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { projects } from '@/db/schema'

export const POST = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const user = await context.session.user
    const body = await req.json()
    
    // Validate input
    if (!body.name || body.name.length < 3) {
      return NextResponse.json(
        { error: 'Name must be at least 3 characters' },
        { status: 400 }
      )
    }
    
    // Create project
    const newProject = await db.insert(projects).values({
      organizationId: organization.id,
      name: body.name,
      createdBy: user.id,
    }).returning()
    
    return NextResponse.json(newProject[0])
  },
  OrganizationRole.enum.admin // Only admins can create
)

PATCH - Update Resource

// src/app/api/app/org/[slug]/settings/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { organizations } from '@/db/schema'
import { eq } from 'drizzle-orm'

export const PATCH = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const body = await req.json()
    
    // Update organization
    await db
      .update(organizations)
      .set({
        name: body.name,
        image: body.image,
        updatedAt: new Date(),
      })
      .where(eq(organizations.id, organization.id))
    
    return NextResponse.json({ success: true })
  },
  OrganizationRole.enum.admin
)

DELETE - Remove Resource

// src/app/api/app/org/[slug]/delete/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'

export const DELETE = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    
    // Delete organization and all related data
    await deleteOrganization(organization.id)
    
    return NextResponse.json({ success: true })
  },
  OrganizationRole.enum.owner // Only owners can delete
)

Role-Based Examples 🎭

Owner-Only Routes

// src/app/api/app/org/[slug]/billing/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'

export const GET = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    
    // Fetch billing information
    const billing = await getBillingInfo(organization.id)
    
    return NextResponse.json(billing)
  },
  OrganizationRole.enum.owner // Owner only
)

Admin-Level Routes

// src/app/api/app/org/[slug]/members/[userId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'

// Remove member (admin or owner)
export const DELETE = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const params = await context.params
    const { userId } = params
    
    // Remove member
    await removeMember(organization.id, userId)
    
    return NextResponse.json({ success: true })
  },
  OrganizationRole.enum.admin
)

// Change member role (admin or owner)
export const PATCH = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const params = await context.params
    const { userId } = params
    const { role } = await req.json()
    
    // Update member role
    await updateMemberRole(organization.id, userId, role)
    
    return NextResponse.json({ success: true })
  },
  OrganizationRole.enum.admin
)

User-Level Routes

// src/app/api/app/org/[slug]/profile/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'

// Any member can view their profile
export const GET = withOrganizationAuthRequired(
  async (req, context) => {
    const { user, organization } = context.session
    
    // Get user's profile in this organization
    const profile = await getUserProfile(user.id, organization.id)
    
    return NextResponse.json(profile)
  }
  // No role requirement = any member can access
)

Advanced Use Cases 💡

Check Plan Quotas

// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'

export const POST = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const body = await req.json()
    
    // Check quota
    const currentProjects = await getProjectCount(organization.id)
    const maxProjects = organization.plan.quotas.projects
    
    if (currentProjects >= maxProjects) {
      return NextResponse.json(
        { error: 'Project limit reached. Please upgrade your plan.' },
        { status: 403 }
      )
    }
    
    // Create project
    const project = await createProject({
      organizationId: organization.id,
      ...body,
    })
    
    return NextResponse.json(project)
  },
  OrganizationRole.enum.admin
)

Custom Role Logic

// src/app/api/app/org/[slug]/projects/[projectId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'

export const DELETE = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const user = await context.session.user
    const params = await context.params
    const { projectId } = params
    
    // Get project
    const project = await getProject(projectId)
    
    // Check permissions: Owner, Admin, or project creator
    const canDelete = 
      organization.role === 'owner' ||
      organization.role === 'admin' ||
      project.createdBy === user.id
    
    if (!canDelete) {
      return NextResponse.json(
        { error: 'Insufficient permissions' },
        { status: 403 }
      )
    }
    
    // Delete project
    await deleteProject(projectId)
    
    return NextResponse.json({ success: true })
  }
)

Validate Organization Ownership

// src/app/api/app/org/[slug]/resources/[resourceId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { NextResponse } from 'next/server'

export const GET = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
    const params = await context.params
    const { resourceId } = params
    
    // Fetch resource
    const resource = await getResource(resourceId)
    
    // Verify resource belongs to this organization
    if (resource.organizationId !== organization.id) {
      return NextResponse.json(
        { error: 'Resource not found' },
        { status: 404 }
      )
    }
    
    return NextResponse.json(resource)
  }
)

Best Practices 💡

1. Always Validate Input

export const POST = withOrganizationAuthRequired(
  async (req, context) => {
    const body = await req.json()
    
    // ✅ Validate input
    if (!body.name || body.name.length < 3) {
      return NextResponse.json(
        { error: 'Invalid input' },
        { status: 400 }
      )
    }
    
    // Process valid data
  }
)

2. Use Appropriate Roles

// ❌ Bad - No role check for sensitive operation
export const DELETE = withOrganizationAuthRequired(async (req, context) => {
  await deleteOrganization()
})

// ✅ Good - Require owner for deletion
export const DELETE = withOrganizationAuthRequired(
  async (req, context) => {
    await deleteOrganization()
  },
  OrganizationRole.enum.owner
)

3. Check Resource Ownership

// ✅ Verify resource belongs to organization
const resource = await getResource(resourceId)
if (resource.organizationId !== organization.id) {
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
}

4. Return Proper Status Codes

// 400 - Bad Request
return NextResponse.json({ error: 'Invalid data' }, { status: 400 })

// 403 - Forbidden (authenticated but no permission)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })

// 404 - Not Found
return NextResponse.json({ error: 'Not found' }, { status: 404 })

5. Check Plan Quotas

// ✅ Enforce plan limits
if (currentUsage >= plan.quotas.limit) {
  return NextResponse.json(
    { error: 'Plan limit reached' },
    { status: 403 }
  )
}

Error Responses 🚨

The wrapper automatically handles these errors:

401 Unauthorized

User is not logged in:

{
  "error": "Unauthorized",
  "message": "You must be logged in to access this resource"
}

403 Forbidden

User doesn't have required role:

{
  "error": "Forbidden",
  "message": "Insufficient permissions"
}

404 Not Found

Organization not found or user not a member:

{
  "error": "Not Found",
  "message": "Organization not found"
}

Troubleshooting 🔧

"Unauthorized" Error

Make sure user is logged in and has session:

// Test with a valid session token
const response = await fetch('/api/app/org/my-org/projects', {
  headers: {
    'Cookie': 'session=...'
  }
})

"Forbidden" Error

Check if user has the required role:

// Verify user's role
const { membership } = await getOrganization(slug)
console.log('User role:', membership.role)

// Make sure role meets requirements
export const PATCH = withOrganizationAuthRequired(
  async (req, context) => {
    // ...
  },
  OrganizationRole.enum.admin // Requires admin or owner
)

"Organization not found"

Verify the slug in the URL matches an existing organization:

// Check URL: /api/app/org/[slug]/...
// Slug must match an organization the user belongs to

Context is Undefined

Make sure you're accessing context correctly:

// ✅ Correct
export const GET = withOrganizationAuthRequired(
  async (req, context) => {
    const organization = await context.session.organization
  }
)

// ❌ Wrong - missing context parameter
export const GET = withOrganizationAuthRequired(
  async (req) => {
    // context is missing!
  }
)

API Reference 📚

Function Signature

withOrganizationAuthRequired(
  handler: (req: Request, context: Context) => Promise<Response>,
  requiredRole: OrganizationRole
): (req: Request, context: RouteContext) => Promise<Response>

Parameters

ParameterTypeRequiredDescription
handlerFunctionYesYour route handler function
requiredRoleOrganizationRoleYesMinimum role required (OrganizationRole.enum.user/admin/owner)

Context Type

{
  session: {
    expires: string
    user: Promise<User>
    organization: Promise<Organization & { role: string, plan: Plan | null }>
  }
  params: Promise<Record<string, string>>
}

Remember to Await

Both session.user, session.organization, and params are Promises! Always await them before use.

Summary 📋

You now know how to:

  • ✅ Protect API routes with organization authentication
  • ✅ Enforce role-based access control
  • ✅ Access organization data in routes
  • ✅ Handle different HTTP methods
  • ✅ Validate input and check quotas
  • ✅ Return proper error responses
  • ✅ Build secure multi-tenant APIs

Secure by Default

Always use withOrganizationAuthRequired for routes that access organization data. Security first! 🔒