---
feature: Admin login
slug: auth
surface: admin
consumer: vue
version: 0.2.0
status: in-progress
brd_version: "Morvia Project Guide"
brd_ids: [ADM-001, ADM-049]
business_rules: []
decisions: [TD-19]
schema_tables: [users, roles, permissions, permission_user, devices, media]
screens: []
related_docs: []
last_updated: 2026-11-15
---

# Admin login — Vue Dashboard Integration Guide

> Base URL: `{HOST}/api/v1/admin`; see [API conventions](../API_CONVENTIONS.md).

## Quick reference

| | |
|---|---|
| Version | 0.2.0 (2026-11-15) |
| Status | In progress; ADM-001, ADM-049, TD-19. |
| Authentication | Email and password; protected endpoints require a bearer JWT, admin account and active account/role. |

| Method | Path | Route name | Access |
|---|---|---|---|
| POST | `/auth/login` | `admin.auth.login` | Public |
| POST | `/auth/refresh` | `admin.auth.refresh` | Bearer admin JWT; no RBAC permission |
| POST | `/auth/logout` | `admin.auth.logout` | Bearer admin JWT; no RBAC permission |

## Requests and behavior

Send JSON and `Accept: application/json`; `Accept-Language: ar` or `en` selects messages. Fields:

| Endpoint | Field | Rules |
|---|---|---|
| login | `login` | required email string |
| login | `password` | required string, minimum 6 |
| logout | `device_token` | optional nullable string, maximum 500 |
| refresh | — | no body |

**Request payload (`POST /auth/login`):**

```json
{
  "login": "admin@info.com",
  "password": "Password123!"
}
```

Login permits `user_type=super_admin` or `user_type=supervisor`. Successful login records `last_login_at`. Failed logins are limited per lowercased email and IP: `ADMIN_LOGIN_ATTEMPTS` (default 5) over `ADMIN_LOGIN_DECAY_SECONDS` (60). Successful login clears this counter. Exceeding the limit gives 429 and `Retry-After` in seconds.

Existing admin JWTs fail protected requests after a password change because their keyed password-version claim no longer matches; sign in again. Existing JWTs without this claim are also rejected.

Refresh rotates the JWT and blacklists the old token. It requires `JWT_BLACKLIST_ENABLED=true` and `JWT_BLACKLIST_GRACE_PERIOD=0`; otherwise it returns 503. The JWT lifetime uses `JWT_TTL` (default 86400 minutes). Logout blacklists the current JWT and optionally deletes its device token. Suspension toggles `is_active`, stores `suspended_at` for admin accounts, blocks login and protected requests, and preserves the record. Reactivation clears the timestamp and restores an otherwise valid token. Inactive roles block protected requests.

## Captured success responses

Captured from real seeded database execution; JWT values are redacted. Messages follow the request locale.

**Login — HTTP 200**

```json
{
  "status": "success",
  "message": "You logged in successfully",
  "data": {
    "id": 1,
    "name": "Admin",
    "email": "admin@info.com",
    "phone_code": "966",
    "phone": "500000001",
    "country": {
      "id": 1,
      "name": "المملكة العربية السعودية",
      "code": "966",
      "flag": "https://ui-avatars.com/api/?name=🏳&background=e5e7eb&color=6b7280&size=128",
      "phone_length": 9,
      "phone_starting_number": "5"
    },
    "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
    "user_type": "Super Admin",
    "is_active": true,
    "is_verified": true,
    "is_banned": false,
    "is_suspended": false,
    "settings": {
      "locale": "ar",
      "allow_notifications": true
    },
    "location": {
      "lat": null,
      "lng": null
    },
    "token": "<jwt-token-redacted>",
    "verification_token": null,
    "permissions": []
  }
}
```

**Refresh — HTTP 200**

```json
{"status":"success","message":"تم تجديد الرمز بنجاح.","data":{"token":"<test JWT redacted>"}}
```

## Errors

| HTTP | `data.error_code` | Meaning |
|---|---|---|
| 401 | — | Missing, invalid, expired, blacklisted or password-revoked JWT. |
| 403 | `ACCOUNT_INACTIVE` | Inactive account on login; protected requests may return a message without a code for inactivity. |
| 403 | `PERMISSION_DENIED` or — | Non-admin protected access, role or RBAC denial. Login denies non-admin without a code. |
| 422 | — | Validation error or incorrect credentials. |
| 429 | — | Login limit reached; read `Retry-After`. |
| 503 | — | Refresh cannot safely blacklist tokens under current configuration. |

## TypeScript

```ts
export type Envelope<T> = { status: 'success' | 'fail' | 'error'; message: string; data: T };
export type LoginInput = { login: string; password: string };
export type RefreshData = { token: string };
export type LoginData = {
  id: number; name: string | null; email: string | null; phone_code: string | null; phone: string | null;
  country: { id: number; name: string | null; code: string; flag: string | null; phone_length: number | null; phone_starting_number: string | null } | null;
  image: string | null; user_type: string | null; is_active: boolean; is_verified: boolean; is_banned: boolean; is_suspended: boolean;
  settings: { locale: string | null; allow_notifications: boolean }; location: { lat: number | null; lng: number | null };
  token: string; verification_token: string | null; permissions: unknown[];
};
```

## Open decisions and known limitations

The login resource returns a numeric ID despite TD-14, and permissions are currently empty. RBAC checks are enforced in all environments. Deployed configuration and dashboard end-to-end behavior are not established by the isolated tests. A suspension reason is not collected. Per owner decision 2026-11-15, admin password recovery is by a super admin setting a new password via the supervisors update endpoint.

## Version history

| Version | Date | Type | Summary |
|---|---|---|---|
| 0.2.0 | 2026-11-12 | MINOR | Email-only login, throttling, refresh and admin suspension enforcement. |
| 0.1.1 | 2026-11-12 | PATCH | Fix media model boot during login. |
| 0.1.0 | 2026-11-12 | DOCS | Existing auth contract. |

## Changelog

### 0.2.0 — 2026-11-12

Added refresh and throttling; enforced admin identity and active status. Existing JWTs without the password-version claim require sign-in again. Dashboard must support token rotation. ADM-001, ADM-049 and TD-19.
