T ToutadminDocumentation

Installer l'édition Node

L'édition Node tourne comme un service permanent : un processus, une base SQLite sur le disque, rien d'autre à installer — ni base de données externe, ni cache, ni file d'attente. Ce tutoriel va du dépôt vide à l'instance en production, y compris ce que la plupart des guides laissent de côté : le service système, le proxy inverse, le TLS, les sauvegardes et la mise à jour.

Ce qu'il faut#

ÉlémentVersionPourquoi
Node.js22 ou plus récentLe produit s'appuie sur son lanceur de tests intégré et sur des API récentes.
Un compilateur Cbuild-essentialbetter-sqlite3 se compile à l'installation, sauf si un binaire précompilé existe pour votre plateforme.
Un disque localSQLite et les fichiers déposés. Jamais un partage réseau : voir plus bas.

Une machine à un cœur et 512 Mo de mémoire suffit pour quelques dizaines de personnes. Ce qui compte n'est pas la puissance mais le disque : il doit être local et sauvegardé.

1. Récupérer et installer#

git clone <votre-dépôt> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev

npm ci plutôt que npm install : il installe exactement ce que le fichier de verrouillage décrit, sans jamais le réécrire. Sur un serveur, une version qui glisse est une panne qu'on ne comprend pas.

Si la compilation échoue

better-sqlite3 est la seule dépendance qui compile du C. sudo apt install -y build-essential python3 résout la quasi-totalité des cas.

2. Configurer#

cp .env.example .env

Tout se règle par variables d'environnement — lues depuis .env, ou posées par votre gestionnaire de service. Aucune n'est obligatoire : sans .env, l'instance démarre sur le port 3000 et vous envoie vers l'assistant.

VariableDéfautCe qu'elle fait
PORT3000Le port d'écoute.
NODE_ENVproduction en production : cookies stricts, pas de traces détaillées.
SESSION_SECRETengendréScelle les sessions. Laissé vide, il est engendré dans data/session.key.
INSTALL_TOKENRéclamé par l'assistant avant d'installer. Recommandé sur un serveur exposé.
TRUST_PROXY1 derrière un proxy de confiance, et seulement là.
DB_PATHdata/app.sqliteLa base.
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIRsous data/Les dossiers de fichiers : photos, CV, coffre-fort, parapheur, pièces reçues, archives.
LOGIN_RATE_LIMIT10Tentatives de connexion par quart d'heure et par adresse.
GLOBAL_RATE_LIMIT300Requêtes par minute et par adresse.
API_RATE_LIMITAppels d'API par minute et par jeton.
SESSION_IDLE_MINUTES60Inactivité au-delà de laquelle la session tombe.
SESSION_MAX_HOURS12Durée absolue qu'aucune activité ne repousse.
ADMIN_EMAIL, ADMIN_PASSWORDInstallation sans interface : crée l'administrateur au démarrage.
Le secret de session

Changer SESSION_SECRET déconnecte tout le monde d'un coup. Posez-le une fois, engendrez-le au hasard (node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"), et sauvegardez-le avec le reste. Ne le laissez jamais dans un dépôt git.

Poser un jeton d'installation#

Entre le premier démarrage et votre passage dans l'assistant, l'instance est à qui la trouve : le premier arrivé crée le compte d'administration. Posez donc INSTALL_TOKEN avant d'ouvrir le port :

node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# puis dans .env :
INSTALL_TOKEN=c3f1…

L'assistant le compare à temps constant — une comparaison ordinaire laisserait deviner le jeton caractère par caractère — et journalise chaque refus. Une fois l'instance installée, l'assistant se referme de lui-même.

Installer sans interface#

Pour un déploiement automatisé, ADMIN_EMAIL et ADMIN_PASSWORD créent l'administrateur au démarrage, sans passer par l'assistant. Retirez-les ensuite : un mot de passe dans l'environnement d'un service est lisible par qui lit ce service.

3. Permissions et emplacement des données#

sudo useradd --system --home /var/www/toutadmin --shell /usr/sbin/nologin toutadmin
sudo chown -R toutadmin:toutadmin /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/data
sudo chmod 640 /var/www/toutadmin/.env

Le code peut rester en lecture seule ; seul data/ doit être inscriptible. SQLite écrit des fichiers voisins (-wal, -shm) : c'est le dossier qui compte, pas seulement la base.

Jamais sur un partage réseau

NFS et SMB mentent sur le verrouillage des fichiers. SQLite s'en remet à ce verrouillage pour empêcher deux écritures simultanées : sur un partage, la base finit par se corrompre sans prévenir. Disque local, toujours — et c'est la sauvegarde qui part ailleurs, pas la base.

4. Le service#

Lancé à la main, le produit s'arrête quand vous fermez le terminal. Confiez-le à systemd :

# /etc/systemd/system/toutadmin.service
[Unit]
Description=Toutadmin
After=network.target

[Service]
Type=simple
User=toutadmin
WorkingDirectory=/var/www/toutadmin
EnvironmentFile=/var/www/toutadmin/.env
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=5

# Le service n'a besoin d'écrire que dans data/.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/toutadmin/data

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now toutadmin
sudo systemctl status toutadmin
journalctl -u toutadmin -f

Les cinq lignes de durcissement ne sont pas décoratives : ProtectSystem=strict rend tout le système de fichiers non inscriptible, et ReadWritePaths rouvre le seul dossier qui doit l'être. Une faille d'écriture arbitraire n'atteint alors que data/.

5. Le proxy inverse#

Ne servez jamais le port 3000 directement sur Internet : il ne fait pas de TLS et n'a aucune raison d'apprendre à en faire.

server {
    listen 443 ssl http2;
    server_name intranet.exemple.fr;

    ssl_certificate     /etc/letsencrypt/live/intranet.exemple.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/intranet.exemple.fr/privkey.pem;

    client_max_body_size 20M;          # dépôts au coffre-fort, pièces reçues

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name intranet.exemple.fr;
    return 301 https://$host$request_uri;
}

Faites aussi écouter le service sur la boucle locale seulement, pour que le port 3000 ne soit joignable que par le proxy.

TLS#

sudo certbot --nginx -d intranet.exemple.fr

Le produit pose l'en-tête Strict-Transport-Security dès que la connexion est chiffrée, et jamais en clair : annoncé depuis une page non chiffrée il ne serait pas lu, et il enfermerait un essai local en https pendant six mois.

TRUST_PROXY va avec le proxy, et seulement avec lui

Cette variable fait croire le serveur sur parole quant à l'adresse d'origine annoncée dans un en-tête. Derrière nginx, c'est ce qu'il faut — sinon toutes les requêtes semblent venir de 127.0.0.1 et les plafonds par adresse ne protègent plus rien. Sans proxy devant, c'est l'inverse : n'importe qui annonce l'adresse de son choix et contourne les plafonds.

6. L'assistant#

Ouvrez votre domaine. Tant qu'aucun compte n'existe, toute adresse renvoie vers /installation. Cinq étapes :

  1. La langue de l'instance, parmi les 16.
  2. Les prérequis, vérifiés et affichés.
  3. L'organisation : le nom affiché partout.
  4. Les congés annuels attribués à chaque nouveau salarié non-freelance.
  5. Le compte d'administration : adresse et mot de passe d'au moins douze caractères.

Dès qu'un compte existe, /installation renvoie à la page de connexion : l'assistant s'est refermé tout seul, il n'y a pas de fichier à supprimer à la main.

7. Le balayage périodique#

Contrairement à l'édition PHP, rien n'est à planifier : le serveur porte son propre ordonnanceur, qui se réveille toutes les heures et fait huit choses.

Ce que fait le balayageGarantie
Ferme les comptes dont le contrat est arrivé à termeVérifié aussi à la connexion et à l'ouverture du tableau de bord
Émet les factures des abonnements arrivés à échéanceUn index unique empêche de facturer deux fois
Transforme les échéances en notificationsUne clé de déduplication : un terme n'alerte qu'une fois
Purge les notifications lues et le journal d'auditSelon la conservation choisie
Relève la boîte aux lettres comptable (IMAP)Si la relève est configurée
Vide la file des webhooks, avec ses réessaisCinq tentatives, puis abandon
Crée la sauvegarde automatique et l'externaliseQuand l'intervalle est écoulé

Chaque opération est idempotente : un balayage rejoué ne facture pas deux fois et ne notifie pas deux fois. C'est ce qui permet de redémarrer le service à n'importe quel moment sans y réfléchir.

8. Vérifier que tout marche#

# les tests (ils n'écrivent pas dans votre base)
npm test

# une instance de démonstration, pour se promener
node scripts/seed-demo.js

Puis, dans l'interface : créez un membre, connectez-vous avec lui, déposez une pièce jointe, lancez une sauvegarde manuelle et vérifiez-la depuis l'écran Sauvegardes. Ces quatre gestes touchent la base, les fichiers, les droits et l'archive.

9. Mettre à jour#

# 1. une sauvegarde d'abord, toujours — depuis l'écran Sauvegardes

# 2. le code
cd /var/www/toutadmin
git pull
npm ci --omit=dev

# 3. redémarrer ; la base se met à jour toute seule au démarrage
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager

Le schéma évolue par migrations idempotentes : rejouer la mise à jour ne casse rien. data/ et .env ne sont jamais touchés.

Revenir en arrière#

Repassez le code à la version précédente et redémarrez. Les migrations n'effacent pas de colonne : une base migrée reste lisible par la version d'avant, sauf mention explicite dans le journal des versions. En cas de doute, restaurez l'archive prise à l'étape 1.

10. Sauvegarder, vraiment#

L'archive tar.gz emporte la base et les cinq dossiers de fichiers. La base est copiée par la sauvegarde en ligne de SQLite, qui produit une copie cohérente même pendant une écriture — l'édition PHP obtient le même résultat par VACUUM INTO. Chaque fichier porte son empreinte SHA-256, vérifiée à la restauration.

Une archive restée sur le serveur qu'elle protège ne protège de rien : configurez une destination distante (FTPS ou Google Drive) depuis l'écran Sauvegardes, et abonnez un webhook à sauvegarde.echec pour être prévenu quand l'externalisation échoue. Le détail est sur la page Sauvegardes et restauration.

Essayer en local d'abord#

npm install
cp .env.example .env
npm run dev        # redémarre à chaque modification

Ouvrez http://localhost:3000. En local, laissez NODE_ENV vide : en production, les cookies sont marqués « sécurisés » et ne seront pas conservés sur une connexion en clair — vous tourneriez en rond sur la page de connexion.

Passer à l'édition PHP, ou en venir#

Les deux éditions partagent le même schéma — 142 tables, 1311 colonnes — et le même format de mot de passe. Arrêtez l'une, copiez app.sqlite et les dossiers de fichiers, démarrez l'autre : il n'y a pas de conversion. Voir Les deux éditions.

Documentation de Toutadmin — construite le 2026-09-13. Site autonome, indépendant du logiciel.