Files
Apiflow-gateway/README.md
T
MichaelandClaude Opus 5.5 ea084034b9 feat: gateway sécurisé, miroir de schéma automatique et sync incrémentale
- Schéma Hostinger recopié automatiquement depuis PG Debian (colonnes, PK,
  index) : plus d'init.sql maintenu à la main (corrige articles.artcentrale,
  mvtart.no_id et les 12 tables manquantes)
- Stratégies : full transactionnel (DELETE + INSERT, garde-fou de volume) et
  window (signature mensuelle + 10 derniers jours) pour les grosses tables
- Attente de la fin de la sync Debian (_run), pg_try_advisory_lock,
  cron 07:30 Europe/Paris, force honoré, alertes webhook
- Dashboard : basic auth, CSP sans script inline, plus d'innerHTML,
  port 3002 non publié (accès via Nginx Proxy Manager)
- Secrets hors du compose (.env), frps épinglé 0.71.0 avec TLS forcé,
  allowPorts et additionalScopes
- Suppression de sync.sh, tables.js, utils.js, init.sql ; README de déploiement

L'ancien mot de passe de la base `post` figure dans l'historique git :
il doit être changé sur le serveur (voir README).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:41:29 +02:00

202 lines
11 KiB
Markdown

# Gateway Hostinger — frps + miroir PostgreSQL
Côté VPS Hostinger. Reçoit le tunnel frp du serveur Debian du magasin et recopie chaque jour
la base PostgreSQL Debian dans le PostgreSQL Hostinger (`postgres_db`).
## Architecture
```
Serveur Debian (magasin) VPS Hostinger (réseau Docker externe nginx_default)
┌──────────────────────────┐ tunnel frp ┌──────────────────────────────────────────────────┐
│ frpc 0.71.0 ─────────────┼── :7000 (TLS) ──▶│ ff_frps (snowdreamtech/frps:0.71.0-alpine) │
│ API Debian :3000 ─────┼──────────────────│ → ff_frps:13000 (API Debian) │
│ PostgreSQL :5432 ─────┼──────────────────│ → ff_frps:15432 (PostgreSQL Debian) │
└──────────────────────────┘ │ │
│ ff_sync_hostinger (Node 22) │
│ lit frps:15432 (source, lecture seule) │
│ écrit postgres_db:5432 (destination) │
│ dashboard :3002 ◀── Nginx Proxy Manager (HTTPS)│
└──────────────────────────────────────────────────┘
```
- Seul le port **7000** est publié sur l'hôte (connexion frpc). Les ports 13000/15432 et le
dashboard 3002 ne sont joignables que depuis le réseau `nginx_default`.
- frps n'accepte que les ports distants 13000 et 15432 (`allowPorts`) et des connexions TLS.
## Fichiers
| Fichier | Rôle |
|---|---|
| `docker-compose.yml` | services `frps` et `sync` (aucun secret) |
| `frps.toml.example` | modèle de config frps → copier en `frps.toml` (ignoré par git) |
| `.env.example` | modèle des variables → copier en `.env` (ignoré par git) |
| `sync/` | service Node du miroir PostgreSQL + dashboard |
| `sync/test/` | tests des fonctions SQL pures (`npm test`) |
L'ancien `init.sql` (schéma écrit à la main) et `sync/tables.js` (liste de tables) ont été
**supprimés** : ils avaient dérivé de la source (colonnes et tables manquantes). Le schéma de la
destination est désormais recopié automatiquement depuis la source à chaque exécution.
`sync/sync.sh` (pg_dump, jamais utilisé) a aussi été supprimé.
## Variables d'environnement (`.env`)
| Variable | Défaut | Description |
|---|---|---|
| `SRC_HOST` / `SRC_PORT` | `frps` / `15432` | PostgreSQL Debian via le tunnel |
| `SRC_DB` / `SRC_USER` / `SRC_PASS` | — (obligatoire) | identifiants source (idéalement rôle lecture seule) |
| `DST_HOST` / `DST_PORT` | `postgres_db` / `5432` | PostgreSQL Hostinger |
| `DST_DB` / `DST_USER` / `DST_PASS` | — (obligatoire) | identifiants destination |
| `SYNC_CRON` | `30 7 * * *` | planification (cron 5 champs) |
| `SYNC_TZ` | `Europe/Paris` | fuseau du cron |
| `EXCLUDE_TABLES` | vide | tables source à ne pas répliquer (`a,b,c`) |
| `WINDOW_TABLES` | liste du code | surcharge des tables « fenêtre » (`table:colonne_date,…`) |
| `WINDOW_RECENT_DAYS` | `10` | jours toujours rechargés pour les tables « fenêtre » |
| `DEBIAN_WAIT_POLL_MIN` / `DEBIAN_WAIT_MAX_MIN` | `2` / `180` | attente de la fin de la sync Debian |
| `DASHBOARD_USER` / `DASHBOARD_PASSWORD` | vide | Basic auth ; **vide = dashboard désactivé (503)** |
| `ALERT_WEBHOOK_URL` | vide | webhook Slack/Discord/Mattermost (POST `{text, content}`) |
## Fonctionnement du miroir
Une exécution (cron, bouton du dashboard ou CLI) :
1. **Verrou** `pg_try_advisory_lock` sur la destination : deux exécutions ne peuvent jamais se
chevaucher, même depuis deux process (service + `docker exec`). La ligne `_run` de `sync_log`
(destination) passe à `running`.
2. **Attente de la sync Debian** : tant que `sync_log` source a `table_name='_run'` et
`status='running'`, on attend (contrôle toutes les 2 min, 3 h max). Si la ligne n'existe pas,
on continue. Après 3 h : alerte, puis copie quand même.
3. **Lecture du catalogue source** (`pg_catalog`) : toutes les tables de base du schéma `public`
(sauf `EXCLUDE_TABLES`), colonnes dans l'ordre avec leur type exact (`format_type`), NOT NULL,
clé primaire, index.
4. Pour chaque table (chacune dans son propre try/catch, résultat écrit dans `sync_log`) :
- **Schéma** : `CREATE TABLE IF NOT EXISTS` (mêmes colonnes et PK), `ADD COLUMN IF NOT EXISTS`
pour les colonnes manquantes (ajoutées NULLables). Un type différent est seulement signalé.
Rien n'est jamais supprimé côté destination (tables ou colonnes en trop conservées).
- **Données** (voir stratégies ci-dessous), en lisant la source par curseur serveur
(`FETCH 5000`) et en insérant par `INSERT` multi-lignes paramétrés
(`floor(60000 / nb_colonnes)` lignes par requête). Les valeurs transitent en texte brut :
aucune perte de précision (timestamps, numeric, bytea…).
- **Index** après la copie : index source recréés avec le même nom (`IF NOT EXISTS`), clé
primaire ajoutée si la table destination n'en avait pas. Si un index utilise `pg_trgm`, on
tente `CREATE EXTENSION IF NOT EXISTS pg_trgm` ; en cas d'échec, ces index sont ignorés
(avertissement).
5. `sync_log` source → table destination **`source_sync_log`** (la table `sync_log` de la
destination est celle du miroir et n'est jamais écrasée).
6. Fin : `_run` = `ok` ou `error`, verrou libéré, **alerte webhook** si au moins une table a échoué.
### Stratégies de copie
**`full`** (toutes les tables sauf les tables « fenêtre ») :
`BEGIN; DELETE FROM; INSERT de toutes les lignes; COMMIT` dans une seule transaction. Grâce au
MVCC, les lecteurs continuent de lire l'ancienne version sans être bloqués (pas de verrou ACCESS
EXCLUSIVE comme avec TRUNCATE) et ne voient jamais une table vide ou partielle ; l'autovacuum
récupère l'espace des anciennes lignes. `lock_timeout` de 60 s par sécurité.
**Garde-fou volume** : si la source a moins de 50 % des lignes de la destination (et la
destination plus de 100 lignes), la table n'est pas copiée et passe en erreur (alerte).
**`window`** (tables de mouvements, configurées dans `sync/config.js` : `mvtart`, `mvtreg`,
`mvtart_motif`, `histo_art_gamme`, `demarques`, `meteo`, `fidelite_jour` ; une table absente de la
source est ignorée) :
1. Signature par mois des deux côtés :
`date_trunc('month', col), count(*), sum(hashtext(ROW(colonnes source)::text)::bigint)`.
2. Garde-fou volume sur le total des lignes.
3. Les **10 derniers jours** sont toujours rechargés (DELETE + INSERT dans une transaction).
4. Chaque mois dont la signature diffère (ou présent d'un seul côté) est rechargé dans sa propre
transaction (`DELETE` du mois + réinsertion depuis la source).
Résultat : coût incrémental chaque nuit, propagation des corrections et suppressions anciennes,
et **remplissage automatique d'une nouvelle colonne** (tous les mois diffèrent une fois, puis plus).
Les deux connexions utilisent les mêmes paramètres de session (`TimeZone=UTC`, `DateStyle=ISO`,
`extra_float_digits=3`…) pour que les signatures soient comparables. Si un type de colonne diffère
entre source et destination, les mois concernés seront rechargés à chaque exécution : corriger le
type à la main côté destination.
**Mode forcé** (bouton « Forcer » avec confirmation, ou `--force`) : garde-fou volume ignoré et
tables « fenêtre » rechargées entièrement (stratégie `full`).
## Dashboard
- Exposé uniquement sur `nginx_default` : dans Nginx Proxy Manager, créer un proxy host vers
`ff_sync_hostinger`, port `3002`, **avec certificat SSL et « Force SSL »** (Basic auth en clair
sinon).
- HTTP Basic auth (`DASHBOARD_USER` / `DASHBOARD_PASSWORD`). Sans ces variables : 503.
- `GET /healthz` sans authentification (healthcheck Docker).
- Bouton « Lancer la sync » = exécution normale ; « Forcer » = mode forcé (confirmation).
- Par table : stratégie, dernière sync, lignes, durée, statut, détail ou erreur.
## Déploiement (VPS Hostinger)
```bash
cd ~/gateway # emplacement du dépôt sur le VPS
git pull
# 1. Secrets (une seule fois, puis à chaque rotation)
cp -n .env.example .env # compléter DST_*, DASHBOARD_*, ALERT_WEBHOOK_URL…
cp -n frps.toml.example frps.toml # si frps.toml n'existe pas encore
# → dans frps.toml existant, ajouter allowPorts / additionalScopes / tls.force (voir l'exemple)
chmod 600 .env frps.toml
# 2. Changer le mot de passe PostgreSQL destination (voir ci-dessous) et le reporter dans .env
# 3. Construire et démarrer
docker compose up -d --build
docker compose ps # ff_sync_hostinger doit passer "healthy"
docker logs -f ff_sync_hostinger
# 4. Première exécution manuelle (facultatif, sinon cron 07:30 Paris)
docker exec ff_sync_hostinger node index.js --once
```
Le premier passage crée les tables/colonnes/index manquants et recharge tous les mois des tables
« fenêtre » : il est plus long que les suivants.
Commandes utiles :
```bash
docker exec ff_sync_hostinger node index.js --once # exécution normale
docker exec ff_sync_hostinger node index.js --force # exécution forcée
docker exec -it postgres_db psql -U post -d post -c "SELECT * FROM sync_log ORDER BY last_sync DESC"
```
## Rotation des secrets
### Token frp
```bash
openssl rand -hex 32
```
1. Mettre le nouveau token dans `frps.toml` (Hostinger) **et** dans `frpc.toml` (Debian).
2. `docker compose restart frps` côté Hostinger, puis redémarrer frpc côté Debian.
3. Vérifier : `docker logs ff_frps` (connexion du client, proxys 13000 et 15432 acceptés).
frpc doit être en **0.71.0** (`snowdreamtech/frpc:0.71.0-alpine`) avec
`auth.additionalScopes = ["HeartBeats", "NewWorkConns"]` et `transport.tls.enable = true`
(valeur par défaut), sinon frps refusera la connexion.
### Mot de passe PostgreSQL `post` (destination)
L'ancien mot de passe de `post` a été commité dans l'historique git de ce dépôt : il doit être
considéré comme public et **changé sur le serveur**.
```bash
NEWPASS=$(openssl rand -hex 24)
docker exec -it postgres_db psql -U post -d post -c "ALTER USER post WITH PASSWORD '$NEWPASS';"
# Reporter $NEWPASS dans .env (DST_PASS) ET dans la config de tout autre service qui utilise ce
# compte (API Hostinger, outils…), puis :
docker compose up -d sync
```
Réécrire l'historique git ne suffit pas (copies existantes) : seule la rotation protège.
### Dashboard
Changer `DASHBOARD_PASSWORD` dans `.env` puis `docker compose up -d sync`.
## Tests
```bash
cd sync && npm ci && npm test # fonctions SQL pures, sans base de données
```