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.
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
token | string | optional | — | Opaque reactivation token from the email link. Ignored if X-Reactivate-Token header is also set. |
Headers
| Header | Mode | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | A — auth-session | JWT from POST /auth/login. Refresh-cookie biore_session also accepted as a fallback. |
X-Reactivate-Token: <token> | B — token-bearer | Wins over body's token field if both are sent. |
If neither a token nor a JWT is found, the request is 401.
Response
200 OK — SuccessOnlyResponseDto
{
"success": true
}| Field | Type | Notes |
|---|---|---|
success | boolean | Always true on 200 |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | error.user.account_not_deactivated | Account status is not DEACTIVATED (already ACTIVE, SUSPENDED, BANNED, or DELETED) |
400 | (token validation) | Token malformed / expired / already used (consumed via Challenge.usedAt single-shot) |
401 | error.guard.missing_auth_header | Both modes failed: no token AND no JWT |
401 | error.guard.invalid_token | Auth-session mode: JWT failed signature verify or wrong token type |
404 | error.user.not_found | Token resolved a userId but the user row is missing |
429 | (throttle) | Rate limit exceeded (10 req/hour) |
Side effects
Mode A — auth-session
- Class-level
JwtAuthGuardis bypassed by the@Public()metadata; identity is resolved manually. - Read
Authorization: Bearer <jwt>header; fall back tobiore_sessioncookie. jwtService.verify(jwt)— accept onlytype === 'access'(or undefined).- Hand off to the shared reactivate path with the resolved
userId.
Mode B — token-bearer
- Read token from
X-Reactivate-Tokenheader; if absent, readbody.token. challengeService.consumeReactivationToken(token)— single-shot consume; returns{ userId }. Replays raise the underlying token-expired / already-used error.- Hand off to the shared reactivate path.
Shared path (both modes)
- Lookup
User; thrownot_foundif missing. - Reject if
User.status !== 'DEACTIVATED'. - Inside one transaction:
UPDATE GDPRRequest SET status = CANCELLED WHERE userId = :userId AND type = DELETION AND status = PENDING(cancels any scheduled deletion).User.status = ACTIVE.
- Audit log:
[account] AUDIT: Reactivated by user {userId}(auth-session) orReactivated via token for user {userId}(token-bearer). - 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
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/user/user.controller.ts | 169–221 (reactivateAccount, dual-mode) |
| DTO (request) | apps/api-core/src/modules/auth/dto/reactivate-validate.dto.ts | ReactivateTokenBearerDto |
| DTO (response) | apps/api-core/src/common/dto/common-response.dto.ts | SuccessOnlyResponseDto |
| Service (auth-session) | apps/api-core/src/modules/user/user.service.ts | 426–445 (reactivateAccount) |
| Service (token-bearer) | apps/api-core/src/modules/user/user.service.ts | 455–477 (reactivateByToken) |
| Token consume | apps/api-core/src/modules/auth/challenge.service.ts | consumeReactivationToken() |
| Pre-flight | apps/api-core/src/modules/auth/auth.controller.ts | 407–423 (GET /auth/reactivate/validate) |
| Prisma models | packages/prisma/prisma/schema.prisma | User.status, GDPRRequest, Challenge |
Deactivate Account
Temporarily deactivate the current account. Profile becomes hidden, all sessions revoked. Reversible via /users/reactivate.
Request Data Export (GDPR)
Right-of-Access (Art. 15) — queue an export of the user's data. Returns a request id that the client polls until COMPLETED, then downloads via the signed-URL endpoint.