Skip to main content

Endpoints

POST /api/auth/disable-account
DELETE /api/auth/disable-account

POST /api/auth/disable-account

Disables the authenticated user’s account. Requires password verification and 2FA if enabled. Cancels all active Polar subscriptions.

Request

password
string
required
User password for verification
verificationCode
string
Required if 2FA is enabled. TOTP code from authenticator app.

Response

success
boolean
Always true on success
message
string
“Account disabled successfully”

DELETE /api/auth/disable-account

Enables a disabled account. Requires password verification and 2FA if enabled. Returns 400 error if account is not currently disabled.

Request

password
string
required
User password for verification
verificationCode
string
Required if 2FA is enabled. TOTP code from authenticator app.

Response

success
boolean
Always true on success
message
string
“Account enabled successfully”

Implementation Details

Code Reference

export async function POST(request: NextRequest) {
  const corsResponse = handleCors(request)
  if (corsResponse) return corsResponse

  const blocked = await protectRoute(request, { requested: 2 })
  if (blocked) return blocked

  const auth = await requireAuth(request)
  if ('error' in auth) return auth.error

  try {
    const body = await request.json()
    const validated = disableAccountSchema.parse(body)

    const user = await prisma.user.findUnique({
      where: { id: auth.userId },
      include: {
        serviceAccess: true,
      },
    })

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

    const isValid = await bcrypt.compare(validated.password, user.passwordHash)
    if (!isValid) {
      logger.warn('Account disable failed: Invalid password', auth.userId)
      return errorResponse('Invalid password', 401, request.headers.get('origin'))
    }

    if (user.twoFactorEnabled) {
      if (!validated.verificationCode) {
        return errorResponse('2FA verification code is required', 401, request.headers.get('origin'))
      }

      if (!user.twoFactorSecret) {
        return errorResponse('2FA is enabled but secret is missing', 500, request.headers.get('origin'))
      }

      const isValid2FA = speakeasy.totp.verify({
        secret: user.twoFactorSecret,
        encoding: 'base32',
        token: validated.verificationCode,
        window: 2,
      })

      if (!isValid2FA) {
        logger.warn('Account disable failed: Invalid 2FA code', auth.userId)
        return errorResponse('Invalid 2FA verification code', 401, request.headers.get('origin'))
      }
    }

    for (const service of user.serviceAccess) {
      if (service.polarSubscriptionId && process.env.POLAR_ACCESS_TOKEN) {
        try {
          const response = await fetch(`https://api.polar.sh/v1/subscriptions/${service.polarSubscriptionId}`, {
            method: 'DELETE',
            headers: {
              'Authorization': `Bearer ${process.env.POLAR_ACCESS_TOKEN}`,
              'Content-Type': 'application/json',
            },
          })
          
          if (response.ok) {
            logger.info(`Canceled Polar subscription: ${service.polarSubscriptionId}`, auth.userId)
          } else {
            logger.warn(`Failed to cancel Polar subscription: ${service.polarSubscriptionId}`, response.status)
          }
        } catch (error) {
          logger.error('Failed to cancel Polar subscription:', error)
        }
      }
    }

    await createAuditLog(auth.userId, 'USER_DISABLE', {
      email: user.email,
    })

    await prisma.user.update({
      where: { id: auth.userId },
      data: {
        disabled: true,
      },
    } as any)

    logger.info(`User account disabled: ${user.email}`, auth.userId)

    return jsonResponse(
      { success: true, message: 'Account disabled successfully' },
      200,
      request.headers.get('origin')
    )
  } catch (error: any) {
    if (error.name === 'ZodError') {
      logger.warn('Disable account validation error:', error.errors)
      return errorResponse(error.errors[0].message, 400, request.headers.get('origin'))
    }
    logger.error('Disable account error:', error)
    return errorResponse('Internal server error', 500, request.headers.get('origin'))
  }
}

Status Codes

200
OK
Success
400
Bad Request
Validation error or account not disabled (DELETE only)
401
Unauthorized
Invalid password or 2FA code
404
Not Found
User not found

Example Requests

Disable Account

curl -X POST https://auth.nullpass.xyz/api/auth/disable-account \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "password": "yourpassword123",
    "verificationCode": "123456"
  }'

Enable Account

curl -X DELETE https://auth.nullpass.xyz/api/auth/disable-account \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "password": "yourpassword123",
    "verificationCode": "123456"
  }'

Difference from Ban

  • Disable: Temporary account suspension, can be re-enabled
  • Ban: Permanent account restriction, requires unban action
Both prevent authentication, but disable is typically used for temporary suspensions while ban is for permanent restrictions.

Security Notes

  • Requires password verification
  • Requires 2FA code if 2FA is enabled
  • All Polar subscriptions are canceled when disabling
  • Disabled accounts cannot authenticate
  • Audit logs are created for disable/enable actions

Audit Events

  • USER_DISABLE: Account disabled (POST)
  • USER_UPDATE: Account enabled (DELETE, with action: 'account_enabled')