---
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 |

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 the isolated PHPUnit feature tests on 2026-11-12 with SQLite and notification/bus fakes; JWT values are redacted. Messages follow the request locale.

**Login — HTTP 200**

```json
{"status":"success","message":"You logged in successfully","data":{"id":1,"name":"Mr. Elmo McDermott Sr.","email":"login@example.test","phone_code":null,"phone":null,"country":null,"image":"https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200","user_type":"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":"<test JWT 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.
