# Data Model: Desk Auth & RBAC (003)

---

## desk_users (nova tabela)

| Coluna | Tipo | Obrigatório | Descrição |
|--------|------|-------------|-----------|
| `id` | INTEGER PK | sim | auto |
| `username` | TEXT UNIQUE | sim | `root`, `admin`, `mini`, `noc` |
| `password_hash` | TEXT | sim | bcrypt |
| `role` | TEXT | sim | `super_admin` \| `ops_lead` \| `technician` \| `noc` |
| `display_name` | TEXT | não | ex. "Roger" para root |
| `active` | INTEGER | sim | 1=activo, 0=desactivado |
| `last_login_at` | TEXT ISO8601 | não | UTC |
| `created_at` | TEXT ISO8601 | sim | UTC |
| `updated_at` | TEXT ISO8601 | sim | UTC |

### Seed inicial

| username | role | display_name | active |
|----------|------|--------------|--------|
| root | super_admin | Roger | 1 |
| admin | ops_lead | Chefe Ops | 1 |
| mini | technician | Suporte | 1 |
| noc | noc | NOC | 1 |

---

## tickets (alteração)

| Coluna nova | Tipo | Descrição |
|-------------|------|-----------|
| `assigned_to` | TEXT NULL | username do técnico responsável |
| `assigned_at` | TEXT ISO8601 NULL | quando foi atribuído |

Migration SQL:

```sql
ALTER TABLE tickets ADD COLUMN assigned_to TEXT;
ALTER TABLE tickets ADD COLUMN assigned_at TEXT;
```

---

## JWT payload

| Claim | Tipo | Descrição |
|-------|------|-----------|
| `sub` | string | username |
| `role` | string | role actual |
| `exp` | int | unix expiry |
| `iat` | int | issued at |

Exemplo decodificado:

```json
{
  "sub": "admin",
  "role": "ops_lead",
  "exp": 1749570000,
  "iat": 1749541200
}
```

---

## Login request / response

### POST /api/v1/auth/login

**Request**:

```json
{
  "username": "admin",
  "password": "805353"
}
```

**Response 200**:

```json
{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 28800,
  "username": "admin",
  "role": "ops_lead",
  "display_name": "Chefe Ops"
}
```

**Response 401**:

```json
{
  "detail": "invalid credentials"
}
```

---

## Role enum

```text
super_admin > ops_lead > technician > noc
```

Ordem hierárquica usada apenas para UI; permissões são explícitas na matriz (não herança automática).

---

## Permission helpers (lógica)

```python
def can_read_tickets(role: str) -> bool:
    return role in ALL_ROLES

def can_patch_ticket(role: str, ticket: dict, username: str) -> bool:
    if role in ("super_admin", "ops_lead"):
        return True
    if role == "technician":
        assignee = ticket.get("assigned_to")
        return assignee is None or assignee == username
    return False  # noc

def can_run_audit(role: str) -> bool:
    return role in ("super_admin", "ops_lead")

def can_manage_users(role: str) -> bool:
    return role == "super_admin"

def should_mask_ticket(role: str) -> bool:
    return role == "noc"
```

---

## Masked ticket (noc view)

Campos removidos ou substituídos em `company_profile`:

| Campo original | Valor noc |
|----------------|-----------|
| `tax_id` | `***` |
| `address` | `{}` |
| `email_billing` | `***` |
| `email_legal` | `***` |
| `phone_landline` | `***` |
| `billing_state` | omitido |
| `payload.funnel_notes[].data.company_profile` | mascarado recursivo |

---

## State: login session (client)

```text
sessionStorage:
  ligbox_ops_token: "<jwt>"
  ligbox_ops_user: {"username","role","display_name","expires_at"}
```

Logout: clear sessionStorage → redirect `/login.html`

---

## Endpoints auth (novos)

| Method | Path | Auth | Roles |
|--------|------|------|-------|
| POST | `/api/v1/auth/login` | público | — |
| POST | `/api/v1/auth/logout` | JWT | all |
| GET | `/api/v1/auth/me` | JWT | all |
| GET | `/api/v1/auth/users` | JWT | super_admin |
| PATCH | `/api/v1/auth/users/{username}` | JWT | super_admin |
