> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nullpass.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Update User Service

> Update user service entitlement (admin only)

## 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

<ParamField path="userId" type="string" required>
  User ID to update
</ParamField>

<ParamField body="service" type="ServiceIdentifier" required>
  Service identifier: `DROP`, `MAILS`, `VAULT`, or `DB`
</ParamField>

<ParamField body="tier" type="string">
  Access tier (e.g., "free", "premium", "enterprise")
</ParamField>

<ParamField body="isPremium" type="boolean">
  Premium access flag
</ParamField>

<ParamField body="accessFlags" type="object">
  Custom access flags (JSON object)
</ParamField>

<ParamField body="metadata" type="object">
  Service-specific metadata (JSON object)
</ParamField>

<ParamField body="customStorageLimit" type="number">
  Custom storage limit in bytes
</ParamField>

<ParamField body="customApiKeyLimit" type="number">
  Custom API key limit
</ParamField>

## Response

<ResponseField name="entitlement" type="object">
  Updated or created service entitlement
</ResponseField>

## 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

```21:123:nullpass_clean/src/app/api/admin/users/[userId]/route.ts theme={null}
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

<ResponseField name="200" type="OK">
  Success
</ResponseField>

<ResponseField name="400" type="Bad Request">
  Validation error
</ResponseField>

<ResponseField name="401" type="Unauthorized">
  Missing or invalid authentication
</ResponseField>

<ResponseField name="403" type="Forbidden">
  Admin access required
</ResponseField>

<ResponseField name="404" type="Not Found">
  User not found
</ResponseField>

## Example Request

```bash theme={null}
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

```json theme={null}
{
  "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)


## OpenAPI

````yaml PATCH /admin/users/{userId}
openapi: 3.1.0
info:
  title: Null Pass API
  description: >-
    Internal API documentation for Null Pass authentication and service
    management system
  version: 1.0.0
servers:
  - url: https://auth.nullpass.xyz/api
security: []
paths:
  /admin/users/{userId}:
    patch:
      tags:
        - Admin
      summary: Update User Service
      description: Update user service entitlement (admin only)
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateServiceRequest'
      responses:
        '200':
          description: Entitlement updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServiceEntitlement'
      security:
        - bearerAuth: []
components:
  schemas:
    UpdateServiceRequest:
      type: object
      required:
        - service
      properties:
        service:
          type: string
          enum:
            - DROP
            - MAILS
            - VAULT
            - DB
        tier:
          type: string
        isPremium:
          type: boolean
        accessFlags:
          type: object
          additionalProperties: true
        metadata:
          type: object
          additionalProperties: true
        customStorageLimit:
          type: integer
        customApiKeyLimit:
          type: integer
    ServiceEntitlement:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        service:
          type: string
          enum:
            - DROP
            - MAILS
            - VAULT
            - DB
        tier:
          type: string
          example: premium
        isPremium:
          type: boolean
        accessFlags:
          type: object
          additionalProperties: true
        metadata:
          type: object
          additionalProperties: true
        customStorageLimit:
          type: integer
          nullable: true
        customApiKeyLimit:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````