Portal do trabalhador — L1
Implementado como escolha de design Aedifica em /app/portal?locale=pt (pt, es, en, fr), fora da shell da app. Substitui a proposta anterior de outra SPA/subdomínio. O portal usa callables Firebase, não endpoints REST /v1/portal.
Acesso e segurança
- Owner/admin escolhe um membro
workere pedeportalInvite({memberId,locale}). O servidor confirma o membro e o email da sua identidade Firebase Auth. Enviaworker-magic-linkpelosendEmailexistente, com a mesma allowlist; não devolve o link ao gestor. Uma identidade Auth pode existir sem palavra-passe nem onboarding completo. - O token aleatório de 256 bits vai no fragmento do URL, removido ao abrir a página, e só o hash SHA-256 fica em
portalLinks/{hash}. Expira em 15 minutos. A troca transacionalportalExchangeLink({token,pin})consome-o uma vez e regista um dispositivo por 30 dias. - PIN de 4–6 dígitos com sal aleatório e scrypt. O dispositivo guarda uma capacidade aleatória; o servidor só conserva o seu hash. Acesso seguinte:
portalLoginPin({orgId,deviceId,deviceSecret,pin}). Cinco falhas bloqueiam o dispositivo; capacidade incorreta não consome tentativas da vítima. Novo convite permite registar outro dispositivo. - A troca devolve custom token com claims
role:worker,orgId,portal:true,deviceId. A app usa uma instância Auth própria com persistência em memória. Reabrir exige PIN e rede; custom/ID tokens não são guardados pela aplicação. Cada chamada revalida membro e dispositivo atuais; revogação impede acesso mesmo com ID token ainda válido. portalListDevices()eportalRevokeDevice({deviceId})exigem owner/admin. A lista devolve apenas metadados (até 100 recentes), nunca hash/sal/capacidade/PIN.
Dados e operações
portalSnapshot()devolve nome, obras atribuídas porfolderMembersou alocação própria de hoje, planeamento próprio de hoje e até 100 registos de horas próprios. Não devolve outros trabalhadores/contactos/documentos comerciais.portalRecordTime({operationId,date,endDate,startTime,endTime,labourBreak,folderId,description,type,half})validatimeInput/schemas.Time, um dia por registo. Identidade é sempre a sessão, estadopending,locked:false; não permite aprovar. A interface oferece trabalho/deslocação/intempérie.- A transação revalida membro, dispositivo e atribuição à obra; usa a guarda
dashboard/time-lock, deteta sobreposições e deduplicaoperationIdpor trabalhador. Mesma operação com dados diferentes é recusada. portalUploadPhoto({projectId,image})recebe base64 até 4 MiB, revalida atribuição, descodifica com limite de píxeis e reencoda JPEG sem EXIF. Guarda emorganizations/{org}/worker-photos/{uid}/{projectId}/{id}.jpg, regista metadados emworkerPhotose acrescenta um ficheiro ao diretório existenteprojectExtras/default, visível em Ficheiros/Galeria da obra. Se a transação falhar, remove o objeto criado.portalLinks,portalDeviceseworkerPhotossão exclusivamente servidor; as regras não expõem credenciais. Os trabalhadores não recebem permissão global de leitura de projetos/horas.
PWA e rede fraca
Manifest e service worker portal-sw.js guardam apenas HTML/JS/CSS/fontes estáticos da shell. Não guardam respostas Firebase/callables, dados, fotografias nem URLs assinados. O build Firebase emite explicitamente os três assets do portal sem incluir o worker MSW.
Horas pendentes persistem localmente por organização/utilizador, com o mesmo operationId até confirmação. A fila reenvia em série ao recuperar rede; entradas confirmadas são eliminadas. Falhas/conflitos ficam visíveis para repetir ou remover/corrigir. Sair mantém pendentes para o mesmo trabalhador; «Esquecer dispositivo» pede confirmação e elimina-os. Fotografias exigem rede e não entram na fila.
Limite explícito: primeiro acesso e PIN exigem rede. O modo offline suporta introduzir horas numa sessão já aberta; reabrir a shell não autentica offline. Não há pedidos de férias, geolocalização, «hoje como ontem» nem endpoints de aprovação novos neste lote; a aprovação existente na app mantém-se.
Operação e testes
Funções: portalInvite, portalExchangeLink, portalLoginPin, portalSnapshot, portalRecordTime, portalUploadPhoto, portalRevokeDevice, portalListDevices. Todas europe-west1, máximo 2 instâncias e concorrência 4; upload 512 MiB. Só portalInvite usa segredo existente RESEND_API_KEY.
O Walter/orquestrador tem de publicar estas funções/regras e assets, confirmar domínio Auth, IAM de criação de custom tokens e permissões do bucket. Este lote não faz deploy nem ativa destinatários reais. Testes unitários e emulador usam um projeto de testes local, com transporte de email simulado.