Files
reservas/docs/ARCHITECTURE.md

8.7 KiB

Arquitetura

Contexto

O portal é uma aplicação independente e complementar ao Snipe-IT. Usa um monólito modular para reduzir custo operacional, mas separa os domínios internamente para evitar acoplamento entre inventário, reservas, aprovações e operações.

Browser
  │ HTTPS
  ▼
Nginx ──► Laravel + Inertia/React
              ├── PostgreSQL (estado do portal)
              ├── Redis (cache, filas, locks)
              ├── SMTP/Mailpit
              └── Snipe-IT REST API

Responsabilidade dos sistemas

Informação/operação Sistema oficial
Etiqueta, série, modelo, categoria e localização habitual Snipe-IT
Estado operacional e atribuição física atual Snipe-IT
Checkout e check-in efetivos Snipe-IT via API
Pedidos futuros e calendário Portal
Aprovação, rejeição, cancelamento e conflitos Portal
Entrega/devolução e evidência operacional Portal, sincronizada com Snipe-IT
Auditoria do fluxo de requisição Portal

Módulos

  • app/Domain/Identity: perfis, permissões e identidades externas.
  • app/Domain/Administration: políticas persistentes, modelos de notificação e configuração funcional.
  • app/Domain/Inventory: cache local de ativos e consultas do catálogo.
  • app/Domain/Reservations: disponibilidade, estados, regras e requisições.
  • app/Domain/Approvals: decisões, histórico e concorrência de aprovação.
  • app/Domain/Operations: entrega, devolução, faltas, atrasos e incidentes.
  • app/Domain/Audit: logs imutáveis pela interface.
  • app/Integrations/SnipeIt: cliente HTTP, DTOs, mapper e exceções da API Snipe-IT.
  • app/Integrations/Microsoft: cliente OIDC de tenant único, descoberta e validação de tokens.

Identidade institucional

O login Microsoft usa Authorization Code Flow com OIDC e PKCE. O portal descobre authorization_endpoint, token_endpoint, emissor e jwks_uri no documento .well-known específico do tenant. O id_token só é aceite após validação RS256, expiração, emissor, audiência, tenant e nonce; state é consumido uma única vez e o fluxo expira ao fim de dez minutos. A rotação de chaves provoca uma atualização imediata do JWKS em cache e uma segunda validação.

A identidade externa é associada explicitamente a uma conta local já autenticada, após confirmação recente da palavra-passe. Uma coincidência de email nunca cria nem associa contas automaticamente. O portal persiste apenas sub, tenant, email apresentado e um subconjunto de claims de perfil; códigos e tokens não são guardados. Utilizadores inativos continuam impedidos de entrar. O login local permanece disponível como contingência operacional.

Consistência temporal

Os períodos usam [starts_at, ends_at): uma reserva que termina às 11:00 não entra em conflito com outra que começa às 11:00.

A defesa ocorre em duas camadas:

  1. AvailabilityService produz erros de negócio legíveis;
  2. PostgreSQL mantém a garantia final com uma restrição EXCLUDE USING gist nos estados bloqueantes.

Uma criação ou aprovação deve bloquear as linhas dos ativos, voltar a validar dentro da transação e tolerar corrida através da restrição da base de dados.

AvailabilityService::availableAssets() aplica as mesmas condições em SQL para alimentar catálogo e formulário sem carregar todo o inventário em memória. O período consultado é propagado até à criação da requisição; a validação transacional continua a ser a garantia aplicacional final.

Pré-reservas ficam bloqueantes em held enquanto aguardam decisão. ReservationService centraliza criação, cancelamento e expiração: todas as transições bloqueiam a requisição, atualizam os itens bloqueantes e criam um AuditLog. Um job único procura prazos vencidos a cada cinco minutos. A restrição GiST continua a proteger contra corridas entre estes ciclos de vida.

ApprovalService bloqueia a requisição e os ativos antes de aprovar ou substituir, revalida a disponibilidade e regista cada passo em approval_actions e audit_logs. Uma substituição cancela o item original e cria um novo held, apenas na mesma categoria. Pedidos e respostas de esclarecimento conservam o histórico e renovam explicitamente o prazo.

Notificações implementam ShouldQueue, usam afterCommit e seguem dois canais: email e tabela notifications. A caixa interna consulta exclusivamente a relação polimórfica do utilizador autenticado, evitando acesso horizontal a notificações de terceiros.

ReservationPolicy fornece defaults versionados e sobreposições guardadas em portal_settings. Antecedência, duração, limite de itens e prazo são validados no pedido e no serviço de domínio. A margem operacional é materializada em reservation_items.buffer_starts_at/buffer_ends_at; a restrição GiST usa estes campos, mantendo a proteção contra corridas. Uma alteração de política só afeta novas reservas, preservando o contrato temporal das existentes.

Os relatórios usam consultas de leitura com período máximo de 366 dias. A exportação CSV é transmitida em blocos e neutraliza conteúdos interpretáveis como fórmulas. Auditoria, relatórios e calendário global têm Gates próprios; o perfil auditor permanece exclusivamente de leitura.

NotificationTemplateService mantém defaults no código e sobreposições em portal_settings. Apenas variáveis conhecidas ({reference}, {requester}, {starts_at}, {ends_at}, {comment}) são substituídas; a alteração de modelos é auditada e nunca inclui credenciais ou configuração SMTP.

Fluxo de dados Snipe-IT

  1. Um job consulta a API paginada com token de leitura.
  2. Um mapper reduz a resposta aos campos documentados.
  3. AssetSyncService faz upsert por snipe_asset_id, calcula content_hash e marca cada ativo com last_seen_sync_run_id.
  4. Apenas após todas as páginas serem recebidas com sucesso, ativos cujo marcador não corresponde à execução atual ficam active = false; não são apagados para preservar histórico.
  5. Antes de aprovação ou entrega, o ativo é validado novamente.
  6. Checkout/check-in usam um token operacional separado e guardam apenas a resposta necessária para auditoria.

HandoverService mantém a ordem explícita “Snipe-IT primeiro, confirmação local depois” para cada equipamento. Uma requisição com vários itens não é apresentada como concluída se apenas parte dos movimentos for aceite: os itens confirmados preservam o seu HandoverRecord, os restantes mantêm o estado anterior e a requisição passa a incident. As escritas externas não têm retry automático porque um timeout pode ocorrer depois de o Snipe-IT executar a operação; repetir sem reconciliação poderia criar um resultado enganador.

O checkout associa o ativo a um utilizador Snipe-IT. O operador confirma o ID remoto na primeira entrega e o portal guarda-o em users.snipe_user_id para utilizações futuras. O check-in repõe o estado e a localização habitual sincronizados do ativo. As respostas externas ficam em handover_records.snipe_response; tokens nunca são persistidos.

Uma falha intermédia pode deixar upserts válidos das páginas anteriores, mas nunca executa a desativação global. A sincronização seguinte converge o cache. Um lock distribuído e um job único impedem duas sincronizações simultâneas.

Referências oficiais usadas para o contrato: autenticação da API, listagem de hardware, limites/paginação, checkout de hardware, check-in de hardware e formato de erros da API.

Segurança

  • Sem registo público.
  • Cookies seguros e HTTPS em produção.
  • Policies em cada ação de domínio.
  • Tokens e segredos apenas no ambiente.
  • Dois tokens Snipe-IT com menor privilégio.
  • MFA obrigatório a definir para perfis privilegiados antes do piloto.
  • Logs de auditoria não expostos a operações de update/delete.

Saúde e recuperação

/health/live confirma que o processo HTTP responde. /health/ready confirma acesso à base de dados e um ciclo de escrita/leitura no cache, devolvendo apenas o estado global e nunca detalhes de infraestrutura. O health check do Nginx usa readiness.

Os scripts em ops/ produzem um pg_dump custom com permissões restritas, validam a estrutura e permitem restaurar para uma base temporária descartável. Encriptação, retenção, cópia externa e agendamento pertencem à plataforma de produção e são acompanhados pelo checklist operacional.