Skip to content

Dispositivos conectados do app

Gestão dos aparelhos mobile conectados a uma conta. Uma conta pode manter vários aparelhos conectados ao mesmo tempo, sem limite. Cada aparelho é uma linha em app_device_sessions, identificada por um deviceId estável gerado e guardado pelo app.

Endpoints

EndpointMétodoAuthDescrição
/app/devicesGETJWT + x-company-idLista os aparelhos ativos (não revogados e com sinal de vida recente) do usuário autenticado
/app/devicesDELETEJWT + x-company-idRevoga todos os aparelhos do usuário de uma vez
/app/devices/:idDELETEJWT + x-company-idRevoga um aparelho específico (desconecta de verdade)
/app/heartbeatPOSTJWTSinal de vida do app; atualiza lastSeenAt para manter o aparelho ativo na lista
/app/exchange-tokenPOSTpúblicaTroca QR/master key por sessão; agora aceita deviceId opcional
/app/mobile-logoutPOSTJWT + x-company-idLogout do próprio app; aceita deviceId opcional para revogar a sessão

GET /app/devices

Retorna a lista de aparelhos ativos do usuário, ordenados pelo último acesso. "Ativo" = sem revokedAt e com lastSeenAt mais recente que o limite de inatividade (APP_SESSION_STALE_THRESHOLD_MS, 5 min). Aparelhos sem sinal de vida há mais que esse limite (app encerrado, sem rede, travado) saem da lista mesmo sem terem sido revogados, e voltam quando o app reaparece e envia heartbeat.

typescript
type AppDeviceResponse = {
  id: string;
  deviceId: string;
  deviceName: string | null;
  brand: string | null;
  modelName: string | null;
  osName: string | null;
  osVersion: string | null;
  connectedAt: string;
  lastSeenAt: string;
};
json
{
  "data": [
    {
      "id": "9f1c0e2a-0000-4000-8000-000000000001",
      "deviceId": "b2d1a7c4-0000-4000-8000-000000000abc",
      "deviceName": "iPhone de João Exemplo",
      "brand": "Apple",
      "modelName": "iPhone 14",
      "osName": "ios",
      "osVersion": "17.2",
      "connectedAt": "2026-06-24T12:00:00.000Z",
      "lastSeenAt": "2026-06-24T13:10:00.000Z"
    }
  ]
}

DELETE /app/devices

Revoga todas as sessões ativas do usuário de uma vez (revokedAt = agora em updateMany). Responde 204 No Content. Emite um único app:disconnected (com deviceId: null e source: "dashboard") para o painel limpar a lista e para todos os apps do usuário deslogarem na próxima ação. É o que sustenta o botão discreto "Desconectar todos" do painel.

DELETE /app/devices/:id

Revoga a sessão do aparelho (revokedAt = agora). Responde 204 No Content. Emite o evento de socket app:disconnected (com o deviceId) para o painel atualizar a lista e para o aparelho alvo deslogar na hora, se estiver online. Se :id não pertencer ao usuário (ou já estiver revogado), responde 404.

POST /app/heartbeat

Body: { deviceId }. Atualiza o lastSeenAt da sessão ativa de (userId, deviceId) para "agora" (updateMany sobre sessões não revogadas). Responde 204 No Content. Não emite socket nem mexe em revokedAt: é apenas um sinal de vida barato. O app envia a cada ~1 min enquanto está em primeiro plano e ao voltar do background. Usa @SkipCompany (não exige x-company-id), pois é liveness e independe do contexto de empresa. Sessão revogada ou inexistente: no-op silencioso (204).

POST /app/exchange-token

Body: { token, deviceId?, deviceInfo? }. Quando deviceId é enviado, o backend faz upsert de uma linha em app_device_sessions por (userId, deviceId) e devolve um accessToken (JWT) que carrega o claim sid (id da sessão de dispositivo). Sem deviceId (ex.: master key de QA), o token é emitido sem sid e segue válido como antes.

POST /app/mobile-logout

Body: { deviceId? }. Com deviceId, revoga a sessão daquele aparelho e emite app:disconnected. Sem deviceId, apenas emite o evento (compatibilidade com versões antigas do app).

Revogação real (claim sid)

O JwtAuthGuard/JwtStrategy valida o claim sid quando presente: se a sessão de dispositivo correspondente não existe ou está revogada, a requisição é rejeitada com 401 "Dispositivo desconectado". É isso que torna a desconexão real — o token do aparelho revogado para de funcionar na próxima requisição, mesmo que o app estivesse offline no momento da revogação. Tokens web (sem sid) não são afetados.

Tabela app_device_sessions

ColunaTipoNullableNotas
idUUIDnãoPK; vira o claim sid do token do app
userIdUUIDnãoFK → users (cascade)
companyIdUUIDsimEmpresa do contexto na hora do pareamento
deviceIdtextnãoIdentificador estável gerado pelo app
deviceNametextsimEx.: "iPhone de João Exemplo"
brandtextsimEx.: "Apple"
modelNametextsimEx.: "iPhone 14"
osNametextsimios | android
osVersiontextsimEx.: "17.2"
lastSeenAttimestamptznãoSinal de vida; atualizado no pareamento e a cada heartbeat do app. Sessão sem heartbeat há > 5 min sai da listagem
revokedAttimestamptzsimPreenchido na desconexão; null = ativo
createdAttimestamptznão
updatedAttimestamptznão

Único: (userId, deviceId). Índice: (userId).

Erros conhecidos

  1. 401 "Dispositivo desconectado" — token com sid cuja sessão foi revogada (ou não existe). O app trata como logout.
  2. 404 no DELETE — :id inexistente ou de outro usuário.
  3. 401 "Token invalido ou ja utilizado" — QR token expirado/reusado no exchange (inalterado).

Regras de negócio

As regras de produto (sem limite de aparelhos, desconexão por aparelho, revogação real) estão em business/app-qr-login.md.