# Arquitetura RX — scoreodonto.com → rx.scoreodonto.com → clientWindows.exe → Wasabi > Documento de **revisão / engenharia reversa** (gerado em 2026-06-12, sem alterar código). > Fonte da verdade: `rx.scoreodonto.com/dental-server` (servidor) lido linha a linha. > O `.exe` do client Windows está em repo separado (Gitea) — aqui seu comportamento é > descrito pelo **protocolo** que o servidor expõe/aceita (Socket.IO + `machine_token`). --- ## 1. Visão geral (quem é quem) ``` ┌──────────────────────────┐ ┌───────────────────────────────────────────┐ │ CLÍNICA (PC com sensor) │ │ rx.scoreodonto.com (dental-server) │ │ │ │ Node + Express + Socket.IO │ │ clientWindows.exe │ WSS │ │ │ - captura RX do sensor │────────▶│ io.on('send-image') │ │ - auth: machine_token │ socket │ → storage.saveImage() │ │ - emite 'send-image' │ .io │ → INSERT images (Postgres dental_images) │ └──────────────────────────┘ │ │ │ HTTP API (/api/images, /api/admin/...) │ ┌──────────────────────────┐ HTTPS │ - lista pacientes/imagens │ │ scoreodonto.com (plugin)│────────▶│ - cria devices (/api/admin/clinics) │ │ - superadmin: config │ proxy │ │ │ global (rx_url+token) │ no nosso│ │ saveImage / getImageBuffer │ │ - backend proxeia leitura│ backend │ ▼ │ └──────────────────────────┘ │ ┌─────────────────────────────────────┐ │ │ │ WASABI (S3) bucket │ │ │ │ key: clinic/client/patient/file.png │ │ │ └─────────────────────────────────────┘ │ │ (+ cache local em ./uploads como fallback)│ └───────────────────────────────────────────┘ ``` Quatro peças: 1. **clientWindows.exe** — instalado no PC da clínica; captura a radiografia e **envia via Socket.IO**. 2. **rx.scoreodonto.com / dental-server** — recebe, persiste metadados no Postgres e o binário no Wasabi; serve listagens e imagens. 3. **Wasabi** — storage S3-compatível (objeto = `clinic/client/patient/arquivo`). 4. **scoreodonto.com (plugin RX)** — administra/visualiza via API, com token global (nosso trabalho recente). --- ## 2. Banco de dados do RX (Postgres `dental_images`, em `database.js`) | Tabela | Campos-chave | Papel | |---|---|---| | **clinics_devices** | `clinic_name`, `email` (UNIQUE), `password_hash`, `pc_name`, `machine_token` (UUID UNIQUE), `is_active`, `last_ip` | **1 linha por (clínica, PC)**. Credencial do client Windows. Criada por `POST /api/admin/clinics`. | | **images** | `image_guid`, `patient_name`, `client_name` (=PC), `clinic_name`, `filename`, `thumb_filename`, `doctor`, `remark`, `enabled`, `created_at` | 1 linha por radiografia. **Filtros de leitura hoje: `client_name` e `patient_name`** (e agora `clinic_name`, ver §7). | | **users** | `username`, `email`, `password_hash`, `is_admin` | Usuários admin/web (painel). | | **api_tokens** | `name`, `token` | **Tokens estáticos de API** (viram admin, não expiram). | | **system_config** | `key`, `value` | Key-value. Guarda `wasabi_config`. | | **gtos / gto_images** | `gto_number`, `patient_name`, … | Documentos GTO por paciente. | --- ## 3. clientWindows.exe — como envia os RX (protocolo) O envio NÃO é HTTP de upload; é **Socket.IO** (`server.js`): 1. **Conexão / auth do socket** (`io.use(...)`, server.js:823): - O client conecta com `handshake.auth.token = machine_token` (UUID gerado no cadastro do device). - Regex confirma UUID → busca em `clinics_devices` → se `is_active`, autoriza. - Opcional: se `email`+`password` também vierem, são validados (compat). - Resultado: `socket.user = { role:'client_device', clinicId, clinicName, pcName }` **vindos do registro do device** (não do client). Atualiza `last_ip`. - Também aceita `CLIENT_API_KEY` (sensor legado) e JWT (painel web / app novo). 2. **Envio da imagem — DIRECT UPLOAD pro Wasabi (fluxo REAL de produção)**: Confirmado pelos logs do client (`🌐 Fazendo Direct Upload pro Wasabi (Bypass VPS)`). O binário **NÃO** trafega pela VPS — vai direto pro Wasabi via URL pré-assinada: 1. Client monitora `C:\ProgramData\RF\Dental Sensor\Images` (Chokidar) e gera thumb local. 2. `socket.emit('request-presigned-urls', { metadata:{guid}, patientData:{name,...} })` → servidor responde `{ originalUrl, thumbnailUrl, filename, thumbFilename }` (`storage.getUploadPresignedUrl`, server.js:1218). **A key do Wasabi já é fixada aqui.** 3. Client faz **PUT direto no Wasabi** (original + thumb) usando as presigned URLs. 4. `socket.emit('image-upload-direct', {...})` → servidor só **grava os metadados** em `images` (server.js:1258). Não recebe os bytes. - Em ambos (2 e 4) o eixo de clínica é o MESMO: `clinicName = socket.user?.clinicName || clientInfo?.clinicName || 'Clínica não identificada'` → vem do **`clinic_name` do device** (machine_token). `clientName` = PC; `patientName` = paciente. > ⚙️ Existe também o caminho legado `socket.on('send-image')` (base64 pela VPS, server.js:1031) > e `POST /api/images/upload` (upload manual). Mas o client Windows real usa o **Direct Upload**. > Há ainda `POST /api/client/login` (JWT 2h `client_device`) para fluxos HTTP. 3.1. **⚠️ Estado real dos dados (2026-06-12, banco compartilhado dev+prod):** - **1 device**: `clinic_name="Consultt Clinic"`, `pc="RX"`. - `images`: **2552 imgs / 503 pacientes com `clinic_name = NULL`** (subiram antes do clinic_name ser setado → Wasabi key `desconhecido/RX/paciente/…`) + **222 imgs / 37 pacientes** sob `"Consultt Clinic"`. → A maioria das radiografias está "órfã" de clínica. --- ## 4. Wasabi storage (`storage.js`) — a lógica que NÃO pode quebrar **Config**: lida do banco em `system_config.wasabi_config` (`{ wasabiAccessKey, wasabiSecretKey, wasabiBucket, wasabiRegion, wasabiEndpoint }`), carregada por `loadConfigFromDb()` (server.js:1428, e ao salvar em `routes/system.js`). SDK: `@aws-sdk/client-s3` com `forcePathStyle:true`, endpoint `s3.wasabisys.com`. **Chave do objeto (CRÍTICO):** ``` key = sanitize(clinic_name) / sanitize(client_name) / sanitize(patient_name) / filename ``` `sanitizePathSegment` troca `/\?#%*:"<>|` por `_`. (storage.js:103, 120, 153, 220, 298, 340, 366, 414) **saveImage** (storage.js:139): 1. grava local em `./uploads/`; 2. se Wasabi ativo → `Upload` (lib-storage) para a key acima; 3. se subiu OK → **apaga o local** (Wasabi vira fonte). Se falhar → mantém local (fallback). **getImageBuffer** (storage.js:190): local primeiro → senão Wasabi (key vinda dos args ou de um `SELECT` em `images` por `filename`/`thumb_filename`) → fallback key na raiz do bucket (arquivos legados) → cacheia local. `getImageUrl`/`getDownloadPresignedUrl` geram **URL assinada (1h)**. **deleteImage**: apaga local (uploads + processed) e a key no Wasabi (+ key raiz, redundância legada). > Wasabi é **opcional**: sem config, tudo funciona só em disco local. Por isso "desligar/errar" > a config do Wasabi não derruba o servidor — só muda onde o binário vive. --- ## 5. Autenticação — matriz completa | Quem | Como autentica | Onde | |---|---|---| | **client Windows (socket)** | `machine_token` (UUID) — suficiente; email+senha opcionais | `io.use` server.js:823 | | **client Windows (HTTP)** | `POST /api/client/login` → JWT 2h `role:client_device` | client-auth.js | | **sensor legado** | `CLIENT_API_KEY` (`rf-dental-secure-key-2026` default) | io.use / `x-api-key` | | **admin / painel web** | usuário+senha → JWT 7d (`is_admin`) **OU** `api_token` estático | auth.js `authenticateToken` | | **público** | sem auth | só `GET /api/images/:id/thumbnail` | `authenticateToken` (auth.js:12): tenta JWT; se falhar, procura o token em `api_tokens` → se achar, injeta um **admin mock** (`is_admin:true`). É isso que o **scoreodonto usa** (token global) para chamar `/api/admin/clinics` e as listagens. --- ## 6. Integração scoreodonto.com (plugin RX) — estado atual - **Config GLOBAL** (só superadmin) em `settings.rx_config_global = { rx_url, api_token }` no backend do scoreodonto. O `api_token` é um **token de `api_tokens` do RX** (admin, não expira). - **Criar PC** (`POST /api/rx/devices` no nosso backend) → `POST {rx_url}/api/admin/clinics` com **`clinic_name = clinicaId`** (a clínica do scoreodonto) + `pc_name` + email/senha por PC. Devolve o `machine_token` que vai no client Windows. - **Ler** (`GET /api/rx/patients`, `/api/rx/images`) → proxeia `/api/images/patients` e `/api/images/by-patient` com `?clinic=` + Bearer `api_token`. Isolamento por vínculo. - **Thumbnails**: públicos, carregados direto de `rx_url`. --- ## 7. ⚠️ Pontos de atenção para NÃO quebrar o que já funciona 1. **`clinic_name` é o eixo de tudo** (Wasabi key + filtro de listagem). Os clients Windows **já em produção** sobem imagens com o `clinic_name` que foi gravado **no cadastro do device deles** (provavelmente o nome legível da clínica). 2. **Descompasso scoreodonto × RX:** o plugin novo cria device com `clinic_name = clinicaId` (ID opaco do scoreodonto). Logo: - Imagens **antigas** estão sob `clinic_name = ` no Wasabi e no `images`. - Imagens **novas** (via plugin) ficariam sob `clinic_name = `. - Filtrar por `?clinic=` **não acha as antigas** (clinic_name diferente) — mas **não quebra** o envio existente (cada client continua com o seu clinic_name). - → Para enxergar clínicas já ativas, é preciso um **mapeamento clinicaId ↔ clinic_name atual** (tabela/coluna de-para), NÃO renomear objetos no Wasabi (arriscado e custoso). 3. **Hierarquia desejada (4 níveis) × Wasabi (3 níveis):** - Desejado: `scoreodonto / consulttclinic@gmail.com / clinicaID / paciente`. - Wasabi real: `clinic_name / client_name / paciente / arquivo` (3 níveis, sem o nível "conta"). - A "conta-mestre" (email) é **identidade de auth** (api_token/users.email), **não faz parte da key**. - Realizar 4 níveis exigiria mudar o esquema de key — **risco alto para objetos já gravados**. Caminho seguro: manter 3 níveis e usar `clinic_name = clinicaId` só para o que for novo, com de-para para o legado. 4. **`npm run dev` do RX = `node server.js` (sem nodemon)** → mudanças no dental-server exigem `docker restart rx_dev_api`. (Em prod o deploy é via `deploy-prod.sh` / webhook.) 5. **Não tocar**: `io.on('send-image')`, `storage.saveImage/getImageBuffer`, o esquema da key, nem o cadastro de `clinics_devices` dos PCs que já enviam. Toda evolução deve ser **aditiva** (novos endpoints/colunas), preservando o caminho do client Windows. --- ## 8. Endpoints-chave (referência rápida) | Endpoint | Auth | Uso | |---|---|---| | `WSS send-image` | machine_token | client Windows envia RX | | `POST /api/admin/clinics` | admin/api_token | cria device (clinic_name, email, pc_name) → machine_token | | `GET /api/admin/clinics` | admin/api_token | lista devices | | `GET /api/images?clinic=&client=&search=` | admin | lista imagens (filtros) | | `GET /api/images/patients?clinic=&search=&page=` | admin | pacientes agrupados | | `GET /api/images/by-patient?name=&clinic=` | admin | imagens de um paciente | | `GET /api/images/:id/thumbnail` | **público** | miniatura | | `GET /api/version` · `/download-client` | público | versão / instalador do client | | `POST /api/auth/login` → JWT · `POST /api/auth/tokens` → api_token | admin | obter credenciais de API | --- ## 9. Multi-tenant 2 níveis: `tenant_id` / `clinica_id` (implementado 2026-06-12, DEV) **Hierarquia** (espelha o scoreodonto, que tem conta multi-clínica no plano): `tenant_id (conta/dono = usuarios.id "u_…") / clinica_id (clinicas.id "c_…") / pc / paciente / arquivo`. O `tenant_id` é resolvido pelo **vínculo `dono*`** da clínica (owner_id está vazio hoje). Para a Consultt: `tenant_id=u_1780870994865_3876n` (Rui Cesar) / `clinica_id=c_1780871001549_koqa3o`. - Colunas `tenant_id` **e** `clinica_id` em `clinics_devices` e `images` (+índices); `clinic_name` vira rótulo. Backfill: device + **2776 imgs** → tenant+clínica da Consultt. - Verificado: `?tenant_id=` (conta) e `?clinica_id=` (clínica) isolam; tenant/clínica inexistente → 0. - scoreodonto: cadastro de device resolve+envia `tenant_id`; proxy de leitura libera **conta** (`tenant_id` = própria conta do dono) ou **clínica** (`clinica_id` com vínculo). (Resto desta seção descreve a base por `clinica_id`.) - **Schema RX**: coluna `clinica_id` em `clinics_devices` e `images` (+ índices). `clinic_name` vira só rótulo legível. Backfill: device + **2776 imgs** existentes → `c_1780871001549_koqa3o`. - **Fluxo**: `POST /api/admin/clinics` grava `clinica_id`; o socket (machine_token) resolve `socket.user.clinicaId` do device; `image-upload-direct` (e o legado `send-image`) gravam `images.clinica_id`. **Wasabi key ainda é `clinic_name/…`** (mudança física = fase de reconciliação, pendente da resposta sobre os PNGs de origem). - **Isolamento**: listagens (`/api/images`, `/patients`, `/by-patient`, `/unique-clinics`) filtram por `?clinica_id=`. Verificado: tenant real retorna dados, tenant inexistente → 0. - **scoreodonto**: `/api/rx/*` envia/filtra por `clinica_id` (e `clinic_name`=nome_fantasia no cadastro); proxy de leitura valida vínculo (`_rxClinicScopeOk`) → 403 fora do tenant. - **Pendente**: reconciliação física no Wasabi (`clinica_id/…`) + remoção do `send-image` legado (após confirmar que nenhum `.exe` em campo usa versão pré-Direct-Upload). ### TL;DR O client Windows manda RX por **Socket.IO autenticado por `machine_token`**; o servidor grava metadados no Postgres e o arquivo no **Wasabi sob `clinic_name/client_name/patient_name/arquivo`**. O scoreodonto administra isso com um **token de API global**, filtrando por `clinic_name = clinicaId`. O risco de integração está em **`clinic_name`**: alinhar o ID do scoreodonto com o `clinic_name` que os PCs já usam, sem renomear nada no Wasabi nem mexer no caminho de envio.