# User Endpoints (`src/user/user.controller.ts`)

All endpoints under the `user` controller. Every route is guarded by `BotDetectionGuard` and `ThrottlerGuard`. Routes marked **auth** require a valid access/refresh cookie (see `auth.docs.md`).

Base URL: `http://localhost:5000`

## Rate Limits (global)

All `/user` routes share the default throttler limits. Specific overrides:

| Endpoint | Limit |
| --- | --- |
| `POST /user/forgot-password` | 3 req/min |
| `POST /user/reset-password` | 5 req/min |
| `POST /user/resend-otp` | 5 req/min |

## POST /user/register

Creates a new user account, initializes a session, sets HTTP-only cookies, and returns the session + user.

**Body** (`RegisterUserDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| name | string | Yes | 2–50 chars |
| email | string | Yes | Valid email format |
| password | string | Yes | Min 8 chars |

**Request**
```json
{ "name": "Jane Doe", "email": "jane.doe@example.com", "password": "P@ssw0rd123!" }
```

**Cookies set**: `happydada` (access, 15 min), `hayyya` (refresh, 7 days)

**Response `201`**
```json
{
  "user_data": {
    "id": "clx123456000008l345678901",
    "name": "Jane Doe",
    "email": "jane.doe@example.com",
    "isOnboarded": false,
    "createdAt": "2026-07-21T15:30:00.000Z",
    "updatedAt": "2026-07-21T15:30:00.000Z"
  },
  "session": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "userId": "clx123456000008l345678901",
    "ipAddress": "127.0.0.1",
    "userAgent": "Mozilla/5.0...",
    "createdAt": "2026-07-21T15:30:00.000Z",
    "expiresAt": "2026-07-28T15:30:00.000Z"
  },
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | Validation failed / email already registered |
| 429 | Too many requests |

## POST /user/login

Authenticates credentials and performs a device-trust check against existing sessions from the same IP.

**Body** (`LoginUserDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| email | string | Yes | Valid email format |
| password | string | Yes | User password |

**Request**
```json
{ "email": "jane.doe@example.com", "password": "P@ssw0rd123!" }
```

**Response — Trusted device `200`**: sets cookies, returns:
```json
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
```

**Response — Untrusted device `200`**: generates a 6-digit OTP, emails it, returns:
```json
{ "requireOtp": true, "emailSentStatus": "success" }
```
`emailSentStatus` is `"success"` or `"failed"`. A `login_block:{ip}` cache entry (60s) is set on success; the `LoginBlockGuard` rejects retries from that IP while it exists.

**Errors**

| Code | Description |
| --- | --- |
| 400 | Invalid credentials / login temporarily blocked |
| 429 | Too many requests |

## POST /user/verify-login-otp

Confirms the 6-digit OTP from an untrusted-device login, creates a session, and sets cookies.

**Body** (`VerifyLoginOtpDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| email | string | Yes | Valid email format |
| code | string | Yes | Exactly 6 digits |

**Request**
```json
{ "email": "jane.doe@example.com", "code": "123456" }
```

**Response `200`**: cookies set; returns `{ user_data, session, access_token }` (same shape as register).

**Errors**

| Code | Description |
| --- | --- |
| 400 | Invalid request / Invalid or expired OTP / OTP code has expired |
| 429 | Too many requests |

## POST /user/forgot-password

Generates a reset OTP, deletes any existing forgot-password OTP for the user, and emails it. Rate limited to 3 req/min.

**Body** (`ForgotPasswordDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| email | string | Yes | Valid email format |

**Request**
```json
{ "email": "jane.doe@example.com" }
```

**Response `201`**
```json
{ "emailSentStatus": "success" }
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | User with this email does not exist |
| 429 | Too many requests |

## POST /user/reset-password

Verifies the reset OTP, updates the password, revokes all other sessions, and establishes a new session on the current device. Rate limited to 5 req/min.

**Body** (`ResetPasswordDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| email | string | Yes | Valid email format |
| code | string | Yes | Exactly 6 digits |
| newPassword | string | Yes | 6–20 chars, strong |

**Request**
```json
{ "email": "jane.doe@example.com", "code": "123456", "newPassword": "P@ssw0rd!" }
```

**Cookies set**: `happydada`, `hayyya`

**Response `201`**: returns `{ user_data, session, access_token }` (same shape as register).

**Errors**

| Code | Description |
| --- | --- |
| 400 | User does not exist / Invalid or expired OTP / weak password |
| 429 | Too many requests |

## GET /user/init-google-auth

Builds the Google OAuth authorization URL with a signed state.

**Query**: `role` — optional (`user` / `agent`) embedded in the OAuth state.

**Response `200`**
```json
{ "url": "https://accounts.google.com/o/oauth2/v2/auth?..." }
```

## GET /user/google-callback

OAuth callback. Exchanges `code` + `state`, creates/logs in the user, sets cookies, and redirects to the frontend.

**Query**: `code`, `state`

**Redirects**

| Condition | Target |
| --- | --- |
| Success | `{FRONTEND_BASE_URL}/auth?status=success&oauth_type=google` |
| Failure | `{FRONTEND_BASE_URL}/auth?status={reason}&oauth_type=google` |
| Missing code | `{FRONTEND_BASE_URL}/auth?status=no-code&oauth_type=google` |

## GET /user/check — auth

Returns the currently authenticated user from the access cookie.

**Response `200`**
```json
{ "user_data": { "id": "...", "name": "Jane Doe", "email": "...", "role": "USER" } }
```

**Errors**

| Code | Description |
| --- | --- |
| 401 | Authentication required |

## POST /user/onboard — auth

Sets onboarding fields. Agent-only fields are locked after being set once (see `common.docs.md` for the service rules).

**Body** (`OnboardUserDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| role | string | No | `user` \| `agent`; set once, then ignored |
| name | string | No | 2–50 chars |
| password | string | No | 6–20 chars, strong; for OAuth users only |
| contact | object | No | `{ callNumber?, whatsappNumber? }` — at least one valid phone |
| socials | object | No | `{ x?, facebook?, instagram?, linkedIn?, tiktok? }` — full https links ≤ 255 chars |
| propertyLocation | object | No | `{ state, city }` — letters/spaces/`.'-`, ≤ 50 chars each |
| propertyCount | string | No | One of `1-5`, `5-10`, `10-20`, `20-100` |

**Request**
```json
{
  "contact": { "callNumber": "+2348012345678" },
  "socials": { "instagram": "https://instagram.com/jane" },
  "propertyLocation": { "state": "Lagos", "city": "Ikeja" },
  "propertyCount": "1-5"
}
```

**Response `200`**
```json
{ "user_data": { "...": "..." } }
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | Validation failed / agent-only field on non-agent / field already set |
| 401 | Authentication required |

## POST /user/onboard/complete — auth

Validates that all required onboarding fields are present, then sets `isOnboarded = true`. Agents must have contact, propertyLocation, propertyCount, profileImage and coverImage; regular users need name + password.

**Response `200`**
```json
{ "user_data": { "...": "..." } }
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | Missing required onboarding fields |
| 401 | Authentication required |

## POST /user/images — auth

Uploads profile/cover images. Multipart form data; each file must be **WebP** and ≤ 2 MB.

**Fields**: `profileImage?` (square, 256–2048px, ratio 0.9–1.1), `coverImage?` (ratio 1.5–2.1, width ≥ 1000)

**Response `200`**
```json
{ "user_data": { "profileImage": "https://res.cloudinary.com/...", "coverImage": "https://res.cloudinary.com/..." } }
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | Images must be WebP files / validation failed |
| 401 | Authentication required |
| 503 | Cloudinary not configured |

## POST /user/profile — auth

Updates an agent's profile details. All fields are optional; only provided fields are updated (overwrites existing values, unlike onboarding which is set-once).

**Body** (`UpdateProfileDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | 2–50 characters |
| `contact` | object | no | `{ callNumber?, whatsappNumber? }` (valid phone, at least one required) |
| `socials` | object | no | `{ x?, facebook?, instagram?, linkedIn?, tiktok? }` (valid `http(s)` URLs, ≤ 255 chars) |
| `propertyLocation` | object | no | `{ state, city }` (letters/` .'-`, 2–50 chars each) |
| `propertyCount` | string | no | One of `"1-5"`, `"5-10"`, `"10-20"`, `"20-100"` |

**Response `200`**
```json
{ "user_data": { "...": "..." } }
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | Validation failed / no profile data provided / contact needs at least one number |
| 401 | Authentication required |
| 403 | Profile updates are for agents only |

## POST /user/logout — auth

Deletes the current session and clears both auth cookies.

**Response `200`**
```json
{ "success": true }
```

**Errors**

| Code | Description |
| --- | --- |
| 401 | Authentication required |

## POST /user/resend-otp

Resends the active OTP for the user's email. Rate limited to 5 req/min. Route is public (no auth cookie needed).

**Body** (`ResendOtpDto`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| email | string | Yes | Valid email format |

**Request**
```json
{ "email": "jane.doe@example.com" }
```

**Response `200`**
```json
{ "emailSentStatus": "success" }
```

**Errors**

| Code | Description |
| --- | --- |
| 400 | No active OTP for this email |
| 429 | Too many requests |
