# 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: "" 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 |