Skip to main content
PATCH
/
admin
/
users
/
{userId}
Update User Service
curl --request PATCH \
  --url https://auth.nullpass.xyz/api/admin/users/{userId} \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "service": "DROP",
  "tier": "<string>",
  "isPremium": true,
  "accessFlags": {},
  "metadata": {},
  "customStorageLimit": 123,
  "customApiKeyLimit": 123
}
'
{
  "id": "<string>",
  "userId": "<string>",
  "service": "DROP",
  "tier": "premium",
  "isPremium": true,
  "accessFlags": {},
  "metadata": {},
  "customStorageLimit": 123,
  "customApiKeyLimit": 123,
  "createdAt": "2023-11-07T05:31:56Z",
  "updatedAt": "2023-11-07T05:31:56Z"
}

Endpoint

PATCH /api/admin/users/[userId]

Overview

Updates or creates a service entitlement for a specific user. Requires admin access via DROP service accessFlags or INTERNAL_SECRET.

Request

userId
string
required
User ID to update
service
ServiceIdentifier
required
Service identifier: DROP, MAILS, VAULT, or DB
tier
string
Access tier (e.g., “free”, “premium”, “enterprise”)
isPremium
boolean
Premium access flag
accessFlags
object
Custom access flags (JSON object)
metadata
object
Service-specific metadata (JSON object)
customStorageLimit
number
Custom storage limit in bytes
customApiKeyLimit
number
Custom API key limit

Response

entitlement
object
Updated or created service entitlement

Authentication

Admin Access via DROP Service

User must have DROP service entitlement with:
  • accessFlags.isNullDropTeam: true
  • accessFlags.nullDropTeamRole: "founder" or "dev"

Internal Secret

Alternatively, use x-internal-secret header with INTERNAL_SECRET value.

Implementation Details

Code Reference

export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ userId: string }> }
) {
  const corsResponse = handleCors(request)
  if (corsResponse) return corsResponse

  const internalSecret = request.headers.get('x-internal-secret')
  const isInternal = INTERNAL_SECRET && internalSecret === INTERNAL_SECRET

  let adminUserId: string | null = null

  if (!isInternal) {
    const auth = await requireAuth(request)
    if ('error' in auth) {
      return errorResponse('Unauthorized', 401, request.headers.get('origin'))
    }

    const dropService = await prisma.userServiceEntitlement.findUnique({
      where: {
        userId_service: {
          userId: auth.userId,
          service: 'DROP',
        },
      },
    })

    const accessFlags = (dropService?.accessFlags as any) || {}
    const isAdmin = accessFlags.isNullDropTeam && ['founder', 'dev'].includes(accessFlags.nullDropTeamRole)

    if (!isAdmin) {
      return errorResponse('Forbidden - Admin access required', 403, request.headers.get('origin'))
    }

    adminUserId = auth.userId
  }

  try {
    const { userId } = await params
    const body = await request.json()
    const validated = updateUserServiceSchema.parse(body)

    const targetUserService = await prisma.userServiceEntitlement.findFirst({
      where: { userId },
    })

    if (!targetUserService) {
      const userExists = await prisma.user.findUnique({
        where: { id: userId },
        select: { id: true },
      })

      if (!userExists) {
        return errorResponse('User not found', 404, request.headers.get('origin'))
      }
    }

    const entitlement = await prisma.userServiceEntitlement.upsert({
      where: {
        userId_service: {
          userId: userId,
          service: validated.service,
        },
      },
      update: {
        ...(validated.tier !== undefined && { tier: validated.tier }),
        ...(validated.isPremium !== undefined && { isPremium: validated.isPremium }),
        ...(validated.accessFlags !== undefined && { accessFlags: validated.accessFlags }),
        ...(validated.metadata !== undefined && { metadata: validated.metadata }),
        ...(validated.customStorageLimit !== undefined && { customStorageLimit: validated.customStorageLimit }),
        ...(validated.customApiKeyLimit !== undefined && { customApiKeyLimit: validated.customApiKeyLimit }),
        updatedAt: new Date(),
      },
      create: {
        userId: userId,
        service: validated.service,
        tier: validated.tier || 'free',
        isPremium: validated.isPremium || false,
        accessFlags: validated.accessFlags || undefined,
        metadata: validated.metadata || undefined,
        customStorageLimit: validated.customStorageLimit || null,
        customApiKeyLimit: validated.customApiKeyLimit || null,
      },
    })

    if (adminUserId) {
      await createAuditLog(adminUserId, 'SERVICE_ACCESS_GRANT', {
        targetUserId: userId,
        service: validated.service,
        changes: validated,
      })
    }

    return jsonResponse({ entitlement }, 200, request.headers.get('origin'))
  } catch (error: any) {
    if (error.name === 'ZodError') {
      logger.warn('Update user service validation error:', error.errors)
      return errorResponse(error.errors[0].message, 400, request.headers.get('origin'))
    }
    logger.error('Update user service error:', error)
    return errorResponse('Internal server error', 500, request.headers.get('origin'))
  }
}

Status Codes

200
OK
Success
400
Bad Request
Validation error
401
Unauthorized
Missing or invalid authentication
403
Forbidden
Admin access required
404
Not Found
User not found

Example Request

curl -X PATCH https://auth.nullpass.xyz/api/admin/users/clx1234567890 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "service": "DROP",
    "tier": "premium",
    "isPremium": true,
    "accessFlags": {
      "isNullDropTeam": false
    }
  }'

Example Response

{
  "entitlement": {
    "userId": "clx1234567890",
    "service": "DROP",
    "tier": "premium",
    "isPremium": true,
    "accessFlags": {
      "isNullDropTeam": false
    },
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-01-02T00:00:00.000Z"
  }
}

Audit Events

  • SERVICE_ACCESS_GRANT: Service entitlement updated (only if authenticated via Bearer token, not internal secret)

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

userId
string
required

Body

application/json
service
enum<string>
required
Available options:
DROP,
MAILS,
VAULT,
DB
tier
string
isPremium
boolean
accessFlags
object
metadata
object
customStorageLimit
integer
customApiKeyLimit
integer

Response

200 - application/json

Entitlement updated

id
string
userId
string
service
enum<string>
Available options:
DROP,
MAILS,
VAULT,
DB
tier
string
Example:

"premium"

isPremium
boolean
accessFlags
object
metadata
object
customStorageLimit
integer | null
customApiKeyLimit
integer | null
createdAt
string<date-time>
updatedAt
string<date-time>