Compare commits

28 Commits

Author SHA1 Message Date
cedric f3b01adc5f deploy: change file permission
Déploiement API BETA / Tests extensions (push) Successful in 11m4s
Déploiement API BETA / Build et déploiement beta (push) Failing after 36s
2026-05-15 18:53:52 +04:00
cedric 35aa06b33b fix: change directus port 2026-05-15 18:53:35 +04:00
cedric a5bb2cb35c deploy: add logs for debug
Déploiement API BETA / Tests extensions (push) Successful in 11m5s
Déploiement API BETA / Build et déploiement beta (push) Failing after 1m59s
2026-05-15 18:33:54 +04:00
cedric 409c32a207 deploy: check directus network
Déploiement API BETA / Tests extensions (push) Successful in 11m8s
Déploiement API BETA / Build et déploiement beta (push) Failing after 1m36s
2026-05-15 18:13:55 +04:00
cedric 6878f1a291 deploy: change health check
Déploiement API BETA / Tests extensions (push) Successful in 11m5s
Déploiement API BETA / Build et déploiement beta (push) Failing after 36s
2026-05-15 18:00:22 +04:00
cedric 9f364bf677 deploy: fix directus port
Déploiement API BETA / Tests extensions (push) Successful in 11m7s
Déploiement API BETA / Build et déploiement beta (push) Failing after 1m35s
2026-05-15 17:38:23 +04:00
cedric 6600a22475 deploy: fix directus access
Déploiement API BETA / Tests extensions (push) Successful in 11m7s
Déploiement API BETA / Build et déploiement beta (push) Failing after 1m30s
2026-05-15 17:00:23 +04:00
cedric 23e20a0015 deploy: change permission
Déploiement API BETA / Tests extensions (push) Successful in 11m7s
Déploiement API BETA / Build et déploiement beta (push) Failing after 43s
2026-05-15 16:43:54 +04:00
cedric 10f3062779 deploy: use sudo to use git
Déploiement API BETA / Tests extensions (push) Successful in 11m5s
Déploiement API BETA / Build et déploiement beta (push) Failing after 35s
2026-05-15 15:45:07 +04:00
cedric f978cad3f6 deploy: sync branche
Déploiement API BETA / Tests extensions (push) Successful in 11m6s
Déploiement API BETA / Build et déploiement beta (push) Failing after 35s
2026-05-15 14:40:34 +04:00
cedric b88f7deb43 deploy: simplify deployment
Déploiement API BETA / Tests extensions (push) Successful in 11m5s
Déploiement API BETA / Build et déploiement beta (push) Failing after 35s
2026-05-15 14:26:56 +04:00
cedric e6ead4e700 chore: remove dist from .gitignore 2026-05-15 14:26:40 +04:00
cedric ba7beabb36 deploy: merge build & deploy steps
Déploiement API BETA / Tests extensions (push) Successful in 11m6s
Déploiement API BETA / Build et déploiement beta (push) Failing after 32s
2026-05-15 14:10:19 +04:00
cedric 54672ecac3 deploy: replce ci by install in test
Déploiement API BETA / Tests extensions (push) Successful in 11m7s
Déploiement API BETA / Build extensions (push) Failing after 45s
Déploiement API BETA / Déploiement beta (push) Has been skipped
2026-05-15 13:56:41 +04:00
cedric c95a2318cb deploy: replace npm ci by npm install
Déploiement API BETA / Tests extensions (push) Failing after 4m55s
Déploiement API BETA / Build extensions (push) Has been skipped
Déploiement API BETA / Déploiement beta (push) Has been skipped
2026-05-15 13:50:39 +04:00
cedric 8719294c1e chore: régénérer les lock files des extensions 2026-05-15 13:50:00 +04:00
cedric 8a922a16f6 deploy: fix npm error
Déploiement API BETA / Tests extensions (push) Failing after 4m56s
Déploiement API BETA / Build extensions (push) Has been skipped
Déploiement API BETA / Déploiement beta (push) Has been skipped
2026-05-15 13:38:02 +04:00
cedric a6facb7f33 deploy: change run-name
Déploiement API BETA / Tests extensions (push) Successful in 11m4s
Déploiement API BETA / Déploiement beta (push) Failing after 8s
2026-05-15 13:02:28 +04:00
cedric d5d2507f7c fix: change dependency install command
Déploiement API BETA / Tests extensions (push) Successful in 11m3s
Déploiement API BETA / Déploiement beta (push) Failing after 7s
2026-05-14 20:34:39 +04:00
cedric 5e2da640c6 deploy: add npm install step for extenstions
Déploiement API BETA / Tests extensions (push) Failing after 4m55s
Déploiement API BETA / Déploiement beta (push) Has been skipped
2026-05-14 19:43:19 +04:00
cedric 9773f88dc6 deploy: add workflow for beta
Déploiement API BETA / Tests extensions (push) Failing after 6m20s
Déploiement API BETA / Déploiement beta (push) Has been skipped
2026-05-14 19:34:46 +04:00
cedric 8c66473371 chore: change port to 8066 2026-05-14 17:23:36 +04:00
cedric 6e12f46add fix: create docker networks 2026-05-14 17:20:21 +04:00
cedric 0751790cbf build: upgrade to directus 11.17.2 2026-04-14 17:38:47 +04:00
cedric 0e56909626 test: tests Vitest pour les extensions Directus (18 tests, 0 échec)
- disallow-votes (13 tests) : mock knex chaînable + VersionsService
  - versionId manquant, version introuvable, version > 3j, version obsolète
  - échec non bloquant de compare(), collection ignorée si ≠ votes
  - items.delete : voteId manquant, vote introuvable, version associée ancienne
- new-user (5 tests) : mock MailService + database
  - MailService absent, EMAIL_NEW_USER absent, email déjà utilisé
  - envoi e-mail admin, fallback URL par défaut, erreur SMTP non bloquante
- vitest.config.mjs : pointe sur extensions/*/src/__tests__/**/*.test.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 06:35:01 +04:00
cedric d59972af91 security: restreindre CORS_ORIGIN à une whitelist explicite
Remplace CORS_ORIGIN=true (toutes origines autorisées) par la valeur
de production https://konstitisyon.nu dans .env.sample.
Documente également la valeur de dev local (http://localhost:3000).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:35:10 +04:00
cedric 154856b2a9 docs: corrige l'URL /websocket et ajoute timeout dans la configuration nginx 2026-01-24 17:58:00 +04:00
cedric df3219c3ba docs: ajout de la documentation pour le déploiement 2026-01-23 23:23:21 +04:00
15 changed files with 1778 additions and 628 deletions
+5 -1
View File
@@ -242,7 +242,11 @@ SESSION_COOKIE_NAME="directus_session_token"
CORS_ENABLED=true
# Value for the Access-Control-Allow-Origin header. Use true to match the Origin header, or provide a domain or a CSV of domains for specific access [false]
CORS_ORIGIN=true
# NE PAS utiliser true en production — lister explicitement les origines autorisées
# Dev local :
# CORS_ORIGIN=http://localhost:3000
# Production :
CORS_ORIGIN=https://konstitisyon.nu
# Value for the Access-Control-Allow-Methods header [GET,POST,PATCH,DELETE]
CORS_METHODS=GET,POST,PATCH,DELETE
+95
View File
@@ -0,0 +1,95 @@
name: Déploiement API BETA
run-name: ${{ gitea.actor }} déploie API BETA
on:
push:
branches:
- dev
jobs:
test:
name: Tests extensions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Installer les dépendances
run: npm install
- name: Lancer les tests
run: npm test
build-and-deploy:
name: Build et déploiement beta
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Build directus-extension-disallow-votes
working-directory: extensions/directus-extension-disallow-votes
run: npm install && npm run build
- name: Build directus-extension-new-user
working-directory: extensions/directus-extension-new-user
run: npm install && npm run build
- name: Committer et pousser les dist
run: |
git config user.name "Gitea Actions"
git config user.email "actions@noreply.gitea"
git add extensions/directus-extension-disallow-votes/dist \
extensions/directus-extension-new-user/dist
git diff --staged --quiet || git commit -m "chore: build extensions [skip ci]"
git push origin dev
- name: Déployer sur le serveur
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
cd ${{ secrets.DEPLOY_PATH }}
echo "==> Correction des permissions"
sudo chown -R "$(whoami):" .
echo "==> Synchronisation branche dev"
git fetch origin dev
git reset --hard origin/dev
echo "==> Réseau Docker"
docker network inspect konstitisyon_network >/dev/null 2>&1 \
|| docker network create konstitisyon_network
echo "==> Initialisation base de données"
[ -f database/data.db ] || cp database/data.sample.db database/data.db
echo "==> Permissions volumes"
chown -R 1000:1000 database uploads
echo "==> Démarrage Directus"
docker compose up -d
echo "==> Vérification santé"
for i in $(seq 1 12); do
if curl -sf http://localhost:8066/server/health | grep -q '"status":"ok"'; then
echo "Déploiement OK"
exit 0
fi
echo " Attente... ($i/12)"
sleep 5
done
echo "Échec : Directus ne répond pas après 60s"
exit 1
+173
View File
@@ -0,0 +1,173 @@
# Deploiement Backend Directus
Guide de deploiement du backend Directus sur un serveur Ubuntu.
## Prerequis
- Ubuntu 20.04+ / Debian 11+
- Acces root ou sudo
- Docker (ou Node.js 22+ pour une installation sans Docker)
- Nom de domaine configure (ex: `api.exemple.com`)
## 1. Installation des dependances
```bash
sudo apt update && sudo apt upgrade -y
# Docker
sudo apt install -y docker.io docker-compose-v2
sudo usermod -aG docker $USER
newgrp docker
# Nginx et Certbot
sudo apt install -y nginx certbot python3-certbot-nginx
```
## 2. Configuration du projet
```bash
# Cloner le projet
git clone <URL_DU_REPO> backend
cd backend
# Configurer l'environnement
cp .env.sample .env
nano .env
```
Variables a modifier dans `.env`:
```env
PUBLIC_URL="https://api.exemple.com"
KEY="<openssl rand -hex 32>"
SECRET="<openssl rand -hex 32>"
```
## 3. Preparation des volumes
```bash
mkdir -p database uploads extensions
chmod 755 database uploads extensions
```
## 4. Demarrage de Directus
```bash
docker compose up -d
docker compose ps
curl http://localhost:8055/server/health
```
## 5. Configuration Nginx
```bash
sudo nano /etc/nginx/sites-available/api.exemple.com
```
```nginx
server {
listen 80;
listen [::]:80;
server_name api.exemple.com;
client_max_body_size 100M;
access_log /var/log/nginx/api.exemple.com.access.log;
error_log /var/log/nginx/api.exemple.com.error.log;
location / {
proxy_pass http://127.0.0.1:8055;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
}
location /websocket {
proxy_pass http://127.0.0.1:8055/websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
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;
proxy_read_timeout 86400;
}
}
```
Activer le site:
```bash
sudo ln -s /etc/nginx/sites-available/api.exemple.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
## 6. Certificat SSL
Verifier le DNS:
```bash
dig +short api.exemple.com
curl -4 ifconfig.me
```
Obtenir le certificat:
```bash
sudo certbot --nginx -d api.exemple.com
```
## 7. Verification
```bash
curl https://api.exemple.com/server/health
```
Acces admin: `https://api.exemple.com/admin`
## Commandes utiles
```bash
# Logs
docker compose logs -f
# Redemarrer
docker compose restart
# Mise a jour
git pull origin main
docker compose down && docker compose up -d
# Backup base de donnees
cp database/data.db database/data.db.backup-$(date +%Y%m%d)
# Backup uploads
tar -czf uploads-backup-$(date +%Y%m%d).tar.gz uploads/
```
## Troubleshooting
### Erreur 502
```bash
docker compose ps
curl http://localhost:8055/server/health
sudo tail -20 /var/log/nginx/api.exemple.com.error.log
```
### Erreur SSL
```bash
dig +short api.exemple.com
sudo certbot certificates
sudo certbot renew --force-renewal
```
+7 -2
View File
@@ -1,10 +1,15 @@
services:
directus:
image: directus/directus:11.14.0
image: directus/directus:11.17.2
ports:
- 8055:8055
- 8066:8055
volumes:
- ./database:/directus/database
- ./uploads:/directus/uploads
- ./extensions:/directus/extensions
env_file: ".env"
networks:
- konstitisyon_network
networks:
konstitisyon_network:
external: true
@@ -1,3 +1,2 @@
.DS_Store
node_modules
dist
@@ -0,0 +1 @@
var e=({filter:e},{services:t})=>{const s=async(e,s,o)=>{if(!e)throw new Error("Lidentifiant de la version est manquant.");const n=await s("directus_versions").select("date_created").where({id:e}).first();if(!n)throw new Error("Version non trouvée.");if(new Date(n.date_created)<new Date(Date.now()-2592e5))throw new Error("Le vote nest plus possible après 3 jours de la création de la version.");try{const{VersionsService:n}=t,r=new n({schema:o,knex:s}),a=await r.compare(e);if(a&&!0===a.outdated)throw new Error("Le vote nest plus possible sur une version obsolète.")}catch(e){if("Le vote nest plus possible sur une version obsolète."===e.message)throw e;console.warn("Could not check version outdated status:",e.message)}};e("items.create",(async(e,{collection:t},{database:o,schema:n})=>("votes"===t&&await s(e.content_version_id,o,n),e))),e("items.update",(async(e,{collection:t},{database:o,schema:n})=>("votes"===t&&await s(e.content_version_id,o,n),e))),e("items.delete",(async(e,{collection:t},{database:o,schema:n})=>{if("votes"===t){const t=e[0];if(!t)throw new Error("Lidentifiant du vote est manquant.");const r=await o("votes").select("content_version_id").where({id:t}).first();if(!r)throw new Error("Vote non trouvé.");await s(r.content_version_id,o,n)}return e}))};export{e as default};
@@ -1283,67 +1283,6 @@
"@types/send": "*"
}
},
"node_modules/@unhead/dom": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.14.tgz",
"integrity": "sha512-FaHCWo9JR4h7PCpSRaXuMC6ifXOuBzlI0PD1MmUcxND2ayDl1d6DauIbN8TUf9TDRxNkrK1Ehb0OCXjC1ZJtrg==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/schema": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.14.tgz",
"integrity": "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA==",
"dev": true,
"peer": true,
"dependencies": {
"hookable": "^5.5.3",
"zhead": "^2.2.4"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/shared": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.14.tgz",
"integrity": "sha512-41Qt4PJKYVrEGOTXgBJLRYrEu3S7n5stoB4TFC6312CIBVedXqg7voHQurn32LVDjpfJftjLa2ggCjpqdqoRDw==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/schema": "1.11.14"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/vue": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.14.tgz",
"integrity": "sha512-6nfi7FsZ936gscmj+1nUB1pybiFMFbnuEFo7B/OY2klpLWsYDUOVvpsJhbu7C3u7wkTlJXglmAk6jdd8I7WgZA==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"unhead": "1.11.14"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
},
"peerDependencies": {
"vue": ">=2.7 || >=3"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
@@ -1419,13 +1358,6 @@
"@vue/shared": "3.5.11"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"dev": true,
"peer": true
},
"node_modules/@vue/reactivity": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.11.tgz",
@@ -2083,13 +2015,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"peer": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2457,13 +2382,6 @@
"node": ">= 0.4"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"peer": true
},
"node_modules/human-signals": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
@@ -3209,29 +3127,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
"integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
@@ -3912,22 +3807,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rollup": {
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-esbuild": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-5.0.0.tgz",
@@ -4357,22 +4236,6 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true
},
"node_modules/unhead": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.14.tgz",
"integrity": "sha512-XmXW0aZyX9kGk9ejCKCSvv/J4T3Rt4hoAe2EofM+nhG+zwZ7AArUMK/0F/fj6FTkfgY0u0/JryE00qUDULgygA==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/dom": "1.11.14",
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14",
"hookable": "^5.5.3"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -4899,33 +4762,6 @@
}
}
},
"node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"dev": true,
"hasInstallScript": true,
"peer": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -4991,16 +4827,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zhead": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz",
"integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==",
"dev": true,
"peer": true,
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
@@ -0,0 +1,231 @@
import {describe, it, expect, vi, beforeEach} from 'vitest'
import hookFactory from '../index.js'
// Helpers pour construire les mocks Directus ---------------------------------
/**
* Construit un mock de database (knex) chaînable.
* Chaque méthode retourne `this` sauf `.first()` qui résout la valeur fournie.
*/
const makeDatabase = (resolvedRow) => {
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => Promise.resolve(resolvedRow))
return db
}
/**
* Construit un mock de VersionsService.
* `compareResult` est la valeur renvoyée par `compare()`.
*/
const makeVersionsService = (compareResult) => ({
VersionsService: class {
compare() {
return Promise.resolve(compareResult)
}
},
})
/**
* Enregistre les callbacks du hook et les expose par événement.
* Permet d'appeler directement `callbacks['items.create'](input, meta, ctx)`.
*/
const mountHook = (services) => {
const callbacks = {}
const filter = (event, handler) => {
callbacks[event] = handler
}
hookFactory({filter}, {services})
return callbacks
}
// Dates de référence ---------------------------------------------------------
const RECENT_DATE = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() // hier
const OLD_DATE = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString() // il y a 4 jours
const SCHEMA = {}
// Tests ----------------------------------------------------------------------
describe('disallow-votes — items.create', () => {
let callbacks
beforeEach(() => {
callbacks = mountHook(makeVersionsService({outdated: false}))
})
it('lève une erreur si versionId est absent', async () => {
const db = makeDatabase(null)
await expect(
callbacks['items.create']({}, {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('identifiant de la version est manquant')
})
it('lève une erreur si la version est introuvable', async () => {
const db = makeDatabase(undefined)
await expect(
callbacks['items.create'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('Version non trouvée')
})
it('lève une erreur si la version a plus de 3 jours', async () => {
const db = makeDatabase({date_created: OLD_DATE})
await expect(
callbacks['items.create'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('3 jours')
})
it('lève une erreur si la version est obsolète', async () => {
const db = makeDatabase({date_created: RECENT_DATE})
callbacks = mountHook(makeVersionsService({outdated: true}))
await expect(
callbacks['items.create'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('version obsolète')
})
it('autorise le vote si la version est récente et valide', async () => {
const db = makeDatabase({date_created: RECENT_DATE})
const input = {content_version_id: 'abc'}
const result = await callbacks['items.create'](
input,
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
})
it('autorise le vote si compare() échoue (non bloquant)', async () => {
const db = makeDatabase({date_created: RECENT_DATE})
const servicesWithBrokenCompare = {
VersionsService: class {
compare() {
return Promise.reject(new Error('service indisponible'))
}
},
}
callbacks = mountHook(servicesWithBrokenCompare)
const input = {content_version_id: 'abc'}
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = await callbacks['items.create'](
input,
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
expect(warnSpy).toHaveBeenCalled()
warnSpy.mockRestore()
})
it('ignore les collections qui ne sont pas "votes"', async () => {
const db = makeDatabase(null)
const input = {content_version_id: 'abc'}
const result = await callbacks['items.create'](
input,
{collection: 'articles'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
expect(db).not.toHaveBeenCalled()
})
})
describe('disallow-votes — items.update', () => {
it('applique la même logique que items.create', async () => {
const db = makeDatabase({date_created: OLD_DATE})
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.update'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('3 jours')
})
})
describe('disallow-votes — items.delete', () => {
it('lève une erreur si voteId est absent', async () => {
const db = makeDatabase(null)
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.delete']([], {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('identifiant du vote est manquant')
})
it('lève une erreur si le vote est introuvable', async () => {
const db = makeDatabase(undefined)
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.delete'](['vote-1'], {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('Vote non trouvé')
})
it('lève une erreur si la version associée au vote a plus de 3 jours', async () => {
let callCount = 0
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => {
callCount++
// Premier appel : lookup du vote → retourne content_version_id
// Deuxième appel : lookup de la version → retourne date ancienne
return Promise.resolve(
callCount === 1
? {content_version_id: 'v-abc'}
: {date_created: OLD_DATE}
)
})
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.delete'](['vote-1'], {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('3 jours')
})
it('autorise la suppression si la version est encore valide', async () => {
let callCount = 0
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => {
callCount++
return Promise.resolve(
callCount === 1
? {content_version_id: 'v-abc'}
: {date_created: RECENT_DATE}
)
})
const callbacks = mountHook(makeVersionsService({outdated: false}))
const input = ['vote-1']
const result = await callbacks['items.delete'](
input,
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
})
})
@@ -1,3 +1,2 @@
.DS_Store
node_modules
dist
+1
View File
@@ -0,0 +1 @@
var e=({filter:e},{services:r,env:a})=>{e("users.create",(async(e,{schema:i},{database:s})=>{if(!r.MailService)return console.error("Le service MailService est manquant."),e;const t=a.EMAIL_NEW_USER,n=a.DIRECTUS_URL||"http://0.0.0.0:8055";if(!t)return console.error("La variable EMAIL_NEW_USER est manquante."),e;const l=await(async(e,r)=>void 0!==await r("directus_users").select("id").where({email:e}).first())(e.email,s);if(l)return console.error("Ladresse e-mail est déjà utilisée."),e;try{new r.MailService({schema:i}).send({to:t,subject:`Nouvel utilisateur : ${e.email}`,text:`Un nouvel utilisateur a été créé :\n Nom: ${e.first_name||"N/A"} Email: ${e.email||"N/A"}\n Pour valider => ${n}/admin/users`,html:`\n <p>Un nouvel utilisateur a été créé :</p>\n <ul>\n <li><strong>Nom:</strong> ${e.first_name||"N/A"} </li>\n <li><strong>Email:</strong> ${e.email||"N/A"}</li>\n </ul>\n <p>Pour valider, <a href="${n}/admin/users" target="_blank"><strong>cliquez ici</strong></a> ou sur ce lien => <a href="${n}/admin/users" target="_blank">${n}/admin/users</a></p>\n `}),console.log("Email envoyé avec succès à",t)}catch(e){console.error("Erreur lors de lenvoi de le-mail via MailService:",e)}return e}))};export{e as default};
-174
View File
@@ -1283,67 +1283,6 @@
"@types/send": "*"
}
},
"node_modules/@unhead/dom": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.14.tgz",
"integrity": "sha512-FaHCWo9JR4h7PCpSRaXuMC6ifXOuBzlI0PD1MmUcxND2ayDl1d6DauIbN8TUf9TDRxNkrK1Ehb0OCXjC1ZJtrg==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/schema": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.14.tgz",
"integrity": "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA==",
"dev": true,
"peer": true,
"dependencies": {
"hookable": "^5.5.3",
"zhead": "^2.2.4"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/shared": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.14.tgz",
"integrity": "sha512-41Qt4PJKYVrEGOTXgBJLRYrEu3S7n5stoB4TFC6312CIBVedXqg7voHQurn32LVDjpfJftjLa2ggCjpqdqoRDw==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/schema": "1.11.14"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@unhead/vue": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.14.tgz",
"integrity": "sha512-6nfi7FsZ936gscmj+1nUB1pybiFMFbnuEFo7B/OY2klpLWsYDUOVvpsJhbu7C3u7wkTlJXglmAk6jdd8I7WgZA==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"unhead": "1.11.14"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
},
"peerDependencies": {
"vue": ">=2.7 || >=3"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
@@ -1419,13 +1358,6 @@
"@vue/shared": "3.5.11"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"dev": true,
"peer": true
},
"node_modules/@vue/reactivity": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.11.tgz",
@@ -2083,13 +2015,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"dev": true,
"peer": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2457,13 +2382,6 @@
"node": ">= 0.4"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true,
"peer": true
},
"node_modules/human-signals": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
@@ -3209,29 +3127,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
"integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
@@ -3912,22 +3807,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rollup": {
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-esbuild": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-5.0.0.tgz",
@@ -4357,22 +4236,6 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true
},
"node_modules/unhead": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.14.tgz",
"integrity": "sha512-XmXW0aZyX9kGk9ejCKCSvv/J4T3Rt4hoAe2EofM+nhG+zwZ7AArUMK/0F/fj6FTkfgY0u0/JryE00qUDULgygA==",
"dev": true,
"peer": true,
"dependencies": {
"@unhead/dom": "1.11.14",
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14",
"hookable": "^5.5.3"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -4899,33 +4762,6 @@
}
}
},
"node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"dev": true,
"hasInstallScript": true,
"peer": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -4991,16 +4827,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zhead": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz",
"integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==",
"dev": true,
"peer": true,
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
@@ -0,0 +1,139 @@
import {describe, it, expect, vi, beforeEach} from 'vitest'
import hookFactory from '../index.js'
// Helpers --------------------------------------------------------------------
const makeDatabase = (existingUser) => {
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => Promise.resolve(existingUser))
return db
}
/**
* Monte le hook et capture le callback `users.create`.
*/
const mountHook = ({services, env}) => {
let callback
const filter = (event, handler) => {
if (event === 'users.create') {
callback = handler
}
}
hookFactory({filter}, {services, env})
return callback
}
const SCHEMA = {}
const BASE_ENV = {
EMAIL_NEW_USER: 'admin@example.com',
DIRECTUS_URL: 'http://localhost:8055',
}
// Tests ----------------------------------------------------------------------
describe('new-user — users.create', () => {
let sendMock
let services
beforeEach(() => {
sendMock = vi.fn()
services = {
MailService: class {
send(options) {
return sendMock(options)
}
},
}
})
it('renvoie input sans envoyer d\'e-mail si MailService est absent', async () => {
const db = makeDatabase(undefined)
const callback = mountHook({services: {}, env: BASE_ENV})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const input = {email: 'user@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('MailService'))
consoleSpy.mockRestore()
})
it('renvoie input sans envoyer d\'e-mail si EMAIL_NEW_USER est absent', async () => {
const db = makeDatabase(undefined)
const callback = mountHook({services, env: {}})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const input = {email: 'user@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('EMAIL_NEW_USER'))
consoleSpy.mockRestore()
})
it('renvoie input sans envoyer d\'e-mail si l\'adresse existe déjà', async () => {
const db = makeDatabase({id: 'existing-user-id'}) // utilisateur trouvé
const callback = mountHook({services, env: BASE_ENV})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const input = {email: 'existing@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(sendMock).not.toHaveBeenCalled()
consoleSpy.mockRestore()
})
it('envoie un e-mail à l\'admin pour un nouvel utilisateur valide', async () => {
const db = makeDatabase(undefined) // aucun utilisateur existant
const callback = mountHook({services, env: BASE_ENV})
const input = {email: 'new@example.com', first_name: 'Alice'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(sendMock).toHaveBeenCalledOnce()
const callArgs = sendMock.mock.calls[0][0]
expect(callArgs.to).toBe('admin@example.com')
expect(callArgs.subject).toContain('new@example.com')
})
it('utilise l\'URL Directus par défaut si DIRECTUS_URL est absent', async () => {
const db = makeDatabase(undefined)
const envWithoutUrl = {EMAIL_NEW_USER: 'admin@example.com'}
const callback = mountHook({services, env: envWithoutUrl})
const input = {email: 'new@example.com'}
await callback(input, {schema: SCHEMA}, {database: db})
const callArgs = sendMock.mock.calls[0][0]
expect(callArgs.html).toContain('http://0.0.0.0:8055')
})
it('renvoie input si l\'envoi de mail échoue (non bloquant)', async () => {
const db = makeDatabase(undefined)
const brokenServices = {
MailService: class {
send() {
throw new Error('SMTP error')
}
},
}
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const callback = mountHook({services: brokenServices, env: BASE_ENV})
const input = {email: 'new@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Erreur'),
expect.any(Error),
)
consoleSpy.mockRestore()
})
})
+1111 -274
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -2,8 +2,10 @@
"name": "api.konstitisyon.nu",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": [],
"author": "",
@@ -12,5 +14,8 @@
"dependencies": {
"directus": "^11.2.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"vitest": "^4.1.4"
}
}
+9
View File
@@ -0,0 +1,9 @@
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: false,
environment: 'node',
include: ['extensions/*/src/__tests__/**/*.test.js'],
},
})