BIO.RE
User

Reactivate Account

Bring a DEACTIVATED account back to ACTIVE. Two auth modes — bearer JWT (logged-in user) OR opaque token from email link (no JWT required). Cancels any pending deletion in the same transaction.

POST /api/v1/users/reactivate — 🔑 Bearer OR 🌐 Token-bearer · Rate limit: 10 req / hour

Restores a DEACTIVATED account to ACTIVE. Two auth modes — the endpoint is decorated @Public() and resolves identity manually so the email-link landing page can call it without a session. If both a JWT and a token are presented, the token wins.

Mode A — auth-session. User has a valid bearer JWT (or biore_session cookie). Send an empty body. Reactivates the JWT subject.

Mode B — token-bearer. User clicked an email link; no JWT is available. Send the opaque token via X-Reactivate-Token header or { "token": "..." } body. The header takes precedence if both are present. Validate the token with GET /auth/reactivate/validate first to render the right landing UI before this POST is fired.

If a pending GDPRRequest DELETION exists for this user, it is cancelled in the same transaction — clicking "Reactivate" before the grace period ends restores the account and aborts the scheduled deletion.

Request

Body — ReactivateTokenBearerDto

All fields optional. Body is {} in auth-session mode.

FieldTypeRequiredValidationNotes
tokenstringoptionalOpaque reactivation token from the email link. Ignored if X-Reactivate-Token header is also set.

Headers

HeaderModeNotes
Authorization: Bearer <accessToken>A — auth-sessionJWT from POST /auth/login. Refresh-cookie biore_session also accepted as a fallback.
X-Reactivate-Token: <token>B — token-bearerWins over body's token field if both are sent.

If neither a token nor a JWT is found, the request is 401.

Response

200 OKSuccessOnlyResponseDto

{
  "success": true
}
FieldTypeNotes
successbooleanAlways true on 200

Errors

HTTPcode / i18nKeyReason
400error.user.account_not_deactivatedAccount status is not DEACTIVATED (already ACTIVE, SUSPENDED, BANNED, or DELETED)
400(token validation)Token malformed / expired / already used (consumed via Challenge.usedAt single-shot)
401error.guard.missing_auth_headerBoth modes failed: no token AND no JWT
401error.guard.invalid_tokenAuth-session mode: JWT failed signature verify or wrong token type
404error.user.not_foundToken resolved a userId but the user row is missing
429(throttle)Rate limit exceeded (10 req/hour)

Side effects

Mode A — auth-session

  1. Class-level JwtAuthGuard is bypassed by the @Public() metadata; identity is resolved manually.
  2. Read Authorization: Bearer <jwt> header; fall back to biore_session cookie.
  3. jwtService.verify(jwt) — accept only type === 'access' (or undefined).
  4. Hand off to the shared reactivate path with the resolved userId.

Mode B — token-bearer

  1. Read token from X-Reactivate-Token header; if absent, read body.token.
  2. challengeService.consumeReactivationToken(token) — single-shot consume; returns { userId }. Replays raise the underlying token-expired / already-used error.
  3. Hand off to the shared reactivate path.

Shared path (both modes)

  1. Lookup User; throw not_found if missing.
  2. Reject if User.status !== 'DEACTIVATED'.
  3. Inside one transaction:
    • UPDATE GDPRRequest SET status = CANCELLED WHERE userId = :userId AND type = DELETION AND status = PENDING (cancels any scheduled deletion).
    • User.status = ACTIVE.
  4. Audit log: [account] AUDIT: Reactivated by user {userId} (auth-session) or Reactivated via token for user {userId} (token-bearer).
  5. Existing sessions stay revoked. The user must log in fresh after reactivation.

Code samples

# Mode B — token-bearer (from email link)
curl -X POST https://api.bio.re/api/v1/users/reactivate \
  -H 'X-Reactivate-Token: abc123-from-email-link' \
  -H 'Content-Type: application/json' \
  -d '{}'
# Mode A — auth-session (logged-in user)
curl -X POST https://api.bio.re/api/v1/users/reactivate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{}'
type ReactivateMode =
  | { mode: 'session'; accessToken: string }
  | { mode: 'token'; token: string };

async function reactivateAccount(input: ReactivateMode): Promise<void> {
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (input.mode === 'session') {
    headers.Authorization = `Bearer ${input.accessToken}`;
  } else {
    headers['X-Reactivate-Token'] = input.token;
  }
  const res = await fetch('https://api.bio.re/api/v1/users/reactivate', {
    method: 'POST',
    headers,
    body: JSON.stringify({}),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Reactivate failed'), {
      code: json?.error?.code,
    });
  }
}
import { useMutation } from '@tanstack/react-query';

export function useReactivateAccount() {
  return useMutation({
    mutationFn: async (input: ReactivateMode) => {
      const headers: Record<string, string> = { 'Content-Type': 'application/json' };
      if (input.mode === 'token') {
        headers['X-Reactivate-Token'] = input.token;
      }
      // 'session' mode: rely on httpOnly biore_session cookie that the browser sends
      const res = await fetch('/api/v1/users/reactivate', {
        method: 'POST',
        headers,
        body: JSON.stringify({}),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Reactivate failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
    },
  });
}

Try it

POST
/api/v1/users/reactivate
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

token?string

Reactivation token from email link (alternative to X-Reactivate-Token header)

Response Body

application/json

application/json

application/json

curl -X POST "https://loading/api/v1/users/reactivate" \  -H "Content-Type: application/json" \  -d '{}'
{
  "success": true
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/user/user.controller.ts169–221 (reactivateAccount, dual-mode)
DTO (request)apps/api-core/src/modules/auth/dto/reactivate-validate.dto.tsReactivateTokenBearerDto
DTO (response)apps/api-core/src/common/dto/common-response.dto.tsSuccessOnlyResponseDto
Service (auth-session)apps/api-core/src/modules/user/user.service.ts426–445 (reactivateAccount)
Service (token-bearer)apps/api-core/src/modules/user/user.service.ts455–477 (reactivateByToken)
Token consumeapps/api-core/src/modules/auth/challenge.service.tsconsumeReactivationToken()
Pre-flightapps/api-core/src/modules/auth/auth.controller.ts407–423 (GET /auth/reactivate/validate)
Prisma modelspackages/prisma/prisma/schema.prismaUser.status, GDPRRequest, Challenge

On this page