Compare commits
20 Commits
428f2d7d9a
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
d5d2507f7c
|
|||
|
5e2da640c6
|
|||
|
9773f88dc6
|
|||
|
8c66473371
|
|||
|
6e12f46add
|
|||
|
0751790cbf
|
|||
|
0e56909626
|
|||
|
d59972af91
|
|||
|
154856b2a9
|
|||
|
df3219c3ba
|
|||
|
fe8b3baf1f
|
|||
|
6a9717008b
|
|||
|
5595587c6f
|
|||
| cb8e918279 | |||
| a06e208bfa | |||
| 5ac4b99e32 | |||
| bc0aafa8b3 | |||
| 90f07c68db | |||
| 67f8c565cc | |||
| 56aef7751a |
+6
-2
@@ -61,7 +61,7 @@ PUBLIC_URL="http://localhost:8055"
|
|||||||
# you need to pass to the database instance.
|
# you need to pass to the database instance.
|
||||||
|
|
||||||
DB_CLIENT="sqlite3"
|
DB_CLIENT="sqlite3"
|
||||||
DB_FILENAME="data.db"
|
DB_FILENAME="./database/data.db"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -242,7 +242,11 @@ SESSION_COOKIE_NAME="directus_session_token"
|
|||||||
CORS_ENABLED=true
|
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]
|
# 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]
|
# Value for the Access-Control-Allow-Methods header [GET,POST,PATCH,DELETE]
|
||||||
CORS_METHODS=GET,POST,PATCH,DELETE
|
CORS_METHODS=GET,POST,PATCH,DELETE
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Déploiement API BETA
|
||||||
|
run-name: ${{ gitea.actor }} est en cours de déploiement 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
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Déploiement beta
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Déployer via SSH
|
||||||
|
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 "==> Pull branche dev"
|
||||||
|
git pull origin dev
|
||||||
|
|
||||||
|
echo "==> Build des extensions"
|
||||||
|
for ext in extensions/directus-extension-disallow-votes extensions/directus-extension-new-user; do
|
||||||
|
echo " Building $ext..."
|
||||||
|
cd "$ext"
|
||||||
|
npm install
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
cd -
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Redémarrage Directus"
|
||||||
|
docker compose restart directus
|
||||||
|
|
||||||
|
echo "==> Vérification santé"
|
||||||
|
sleep 5
|
||||||
|
curl -sf http://localhost:8066/server/health | grep -q '"status":"ok"'
|
||||||
|
echo "Déploiement OK"
|
||||||
+173
@@ -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
|
||||||
|
```
|
||||||
@@ -1 +1,169 @@
|
|||||||
# api.konstitisyon.la
|
# api.konstitisyon.nu
|
||||||
|
|
||||||
|
Backend Directus pour konstitisyon.nu, une plateforme de rédaction constitutionnelle collaborative. Ce dépôt contient la configuration Directus et le schéma de la base de données.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Technologies
|
||||||
|
- **Backend**: Directus 11.x
|
||||||
|
- **Base de données**: SQLite
|
||||||
|
- **Authentification**: JWT via Directus
|
||||||
|
|
||||||
|
### Structure du projet
|
||||||
|
```
|
||||||
|
api.konstitisyon.nu/
|
||||||
|
├── extensions/ # Extensions Directus personnalisées
|
||||||
|
├── uploads/ # Fichiers uploadés
|
||||||
|
├── data.db # Base de données SQLite
|
||||||
|
└── .env # Configuration de l'environnement
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modèle de données
|
||||||
|
|
||||||
|
### Schéma de la base de données
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
TITRES ||--o{ ARTICLES : contient
|
||||||
|
TITRES ||--o{ COMMENTAIRES : discute
|
||||||
|
TITRES {
|
||||||
|
uuid id PK
|
||||||
|
string status
|
||||||
|
uuid user_created FK
|
||||||
|
datetime date_created
|
||||||
|
text contenu
|
||||||
|
integer numero
|
||||||
|
}
|
||||||
|
ARTICLES {
|
||||||
|
uuid id PK
|
||||||
|
string status
|
||||||
|
uuid user_created FK
|
||||||
|
datetime date_created
|
||||||
|
text contenu
|
||||||
|
integer numero
|
||||||
|
uuid titre FK
|
||||||
|
}
|
||||||
|
COMMENTAIRES {
|
||||||
|
uuid id PK
|
||||||
|
string status
|
||||||
|
uuid user_created FK
|
||||||
|
datetime date_created
|
||||||
|
text contenu
|
||||||
|
uuid titre FK
|
||||||
|
}
|
||||||
|
VOTES {
|
||||||
|
uuid id PK
|
||||||
|
uuid user_created FK
|
||||||
|
datetime date_created
|
||||||
|
datetime date_updated
|
||||||
|
uuid content_version_id FK
|
||||||
|
integer vote
|
||||||
|
}
|
||||||
|
DIRECTUS_VERSIONS ||--o{ VOTES : evalue
|
||||||
|
DIRECTUS_USERS ||--o{ VOTES : cree
|
||||||
|
DIRECTUS_USERS ||--o{ COMMENTAIRES : ecrit
|
||||||
|
DIRECTUS_USERS ||--o{ ARTICLES : redige
|
||||||
|
DIRECTUS_USERS ||--o{ TITRES : redige
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collections Directus
|
||||||
|
|
||||||
|
#### Collections principales
|
||||||
|
- **Titres**
|
||||||
|
- Sections principales de la constitution
|
||||||
|
- Numérotés et versionnés
|
||||||
|
- Champs : numero, contenu, status
|
||||||
|
|
||||||
|
- **Articles**
|
||||||
|
- Contenus détaillés sous chaque titre
|
||||||
|
- Liés à un titre parent
|
||||||
|
- Champs : numero, contenu, titre (FK), status
|
||||||
|
|
||||||
|
#### Collections participatives
|
||||||
|
- **Commentaires**
|
||||||
|
- Discussions sur les titres
|
||||||
|
- Liés à un titre spécifique
|
||||||
|
- Champs : contenu, titre (FK), user_created
|
||||||
|
|
||||||
|
- **Votes**
|
||||||
|
- Évaluation des versions de contenu
|
||||||
|
- Valeurs : +1 ou -1
|
||||||
|
- Champs : content_version_id (FK), vote, user_created
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
- Node.js 16+
|
||||||
|
- npm ou yarn
|
||||||
|
- SQLite 3
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
Copier le fichier `.env.sample` vers un fichier `.env` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.sample .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Démarrage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installation des dépendances
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Démarrage du serveur
|
||||||
|
npx directus start
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Points d'entrée principaux
|
||||||
|
|
||||||
|
#### Titres
|
||||||
|
- `GET /items/titres` : Liste des titres
|
||||||
|
- `GET /items/titres/:id` : Détails d'un titre
|
||||||
|
- `POST /items/titres` : Créer un titre
|
||||||
|
- `PATCH /items/titres/:id` : Modifier un titre
|
||||||
|
|
||||||
|
#### Articles
|
||||||
|
- `GET /items/articles` : Liste des articles
|
||||||
|
- `GET /items/articles/:id` : Détails d'un article
|
||||||
|
- `POST /items/articles` : Créer un article
|
||||||
|
- `PATCH /items/articles/:id` : Modifier un article
|
||||||
|
|
||||||
|
#### Commentaires
|
||||||
|
- `GET /items/commentaires` : Liste des commentaires
|
||||||
|
- `GET /items/commentaires/:id` : Détails d'un commentaire
|
||||||
|
- `POST /items/commentaires` : Créer un commentaire
|
||||||
|
|
||||||
|
#### Votes
|
||||||
|
- `GET /items/votes` : Liste des votes
|
||||||
|
- `GET /items/votes/:id` : Détails d'un vote
|
||||||
|
- `POST /items/votes` : Créer un vote
|
||||||
|
- `PATCH /items/votes/:id` : Modifier un vote
|
||||||
|
|
||||||
|
### Authentification
|
||||||
|
|
||||||
|
- `POST /auth/login` : Connexion utilisateur
|
||||||
|
- `POST /auth/refresh` : Rafraîchissement du token
|
||||||
|
- `POST /auth/logout` : Déconnexion
|
||||||
|
|
||||||
|
### Base de données
|
||||||
|
|
||||||
|
Le projet utilise une base de données SQLite. Un fichier `data.sample.db` est fourni avec :
|
||||||
|
- La structure complète de la base de données
|
||||||
|
- Un compte administrateur par défaut :
|
||||||
|
- Email : admin@example.com
|
||||||
|
- Mot de passe : admin
|
||||||
|
|
||||||
|
Pour démarrer un nouveau projet :
|
||||||
|
1. Copier `data.sample.db` vers `data.db`
|
||||||
|
- `cp data.sample.db data.db`
|
||||||
|
2. Mettre à jour le fichier `.env` pour pointer vers `data.db`
|
||||||
|
3. Démarrer le serveur
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Ce projet est sous licence AGPL-3. Cette licence garantit que le code reste libre et que toute modification doit être partagée avec la communauté. Voir le fichier `LICENSE` pour plus de détails.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Pour toute question ou suggestion concernant le projet, n'hésitez pas à ouvrir une issue sur Codeberg.
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
directus:
|
||||||
|
image: directus/directus:11.17.2
|
||||||
|
ports:
|
||||||
|
- 8066:8066
|
||||||
|
volumes:
|
||||||
|
- ./database:/directus/database
|
||||||
|
- ./uploads:/directus/uploads
|
||||||
|
- ./extensions:/directus/extensions
|
||||||
|
env_file: ".env"
|
||||||
|
networks:
|
||||||
|
- konstitisyon_network
|
||||||
|
networks:
|
||||||
|
konstitisyon_network:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "disallow-votes",
|
||||||
|
"description": "Interdit les votes sur des versions ayant plus de 3 jours",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-hook"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "hook",
|
||||||
|
"path": "dist/index.js",
|
||||||
|
"source": "src/index.js",
|
||||||
|
"host": "^10.10.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w --no-minify",
|
||||||
|
"link": "directus-extension link"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "12.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
export default ({filter}, {services}) => {
|
||||||
|
const checkVersionValidity = async (versionId, database, schema) => {
|
||||||
|
if (!versionId) {
|
||||||
|
throw new Error('L’identifiant de la version est manquant.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = await database('directus_versions')
|
||||||
|
.select('date_created')
|
||||||
|
.where({id: versionId })
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
throw new Error('Version non trouvée.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if version is older than 3 days
|
||||||
|
const createdAt = new Date(version.date_created)
|
||||||
|
const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
if (createdAt < threeDaysAgo) {
|
||||||
|
throw new Error(
|
||||||
|
'Le vote n’est plus possible après 3 jours de la création de la version.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if version is outdated (has been superseded by a promoted version)
|
||||||
|
try {
|
||||||
|
const {VersionsService} = services
|
||||||
|
const versionsService = new VersionsService({schema, knex: database})
|
||||||
|
const comparison = await versionsService.compare(versionId)
|
||||||
|
|
||||||
|
if (comparison && comparison.outdated === true) {
|
||||||
|
throw new Error(
|
||||||
|
'Le vote n’est plus possible sur une version obsolète.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If it's our custom error, rethrow it
|
||||||
|
if (error.message === 'Le vote n’est plus possible sur une version obsolète.') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
// Otherwise, log and continue (don't block vote if comparison fails)
|
||||||
|
console.warn('Could not check version outdated status:', error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter('items.create', async (input, {collection}, {database, schema}) => {
|
||||||
|
if (collection === 'votes') {
|
||||||
|
await checkVersionValidity(input.content_version_id, database, schema)
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
})
|
||||||
|
|
||||||
|
filter('items.update', async (input, {collection}, {database, schema}) => {
|
||||||
|
if (collection === 'votes') {
|
||||||
|
await checkVersionValidity(input.content_version_id, database, schema)
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
})
|
||||||
|
|
||||||
|
filter('items.delete', async (input, {collection}, {database, schema}) => {
|
||||||
|
if (collection === 'votes') {
|
||||||
|
const voteId = input[0]
|
||||||
|
|
||||||
|
if (!voteId) {
|
||||||
|
throw new Error('L’identifiant du vote est manquant.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = await database('votes')
|
||||||
|
.select('content_version_id')
|
||||||
|
.where({id: voteId })
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if (!vote) {
|
||||||
|
throw new Error('Vote non trouvé.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkVersionValidity(vote.content_version_id, database, schema)
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
EMAIL_NEW_USER=
|
||||||
|
DIRECTUS_URL="http://0.0.0.0:8055"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
+5014
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "directus-extension-new-user",
|
||||||
|
"description": "Envoie un e-mail lors de l’inscription d’un utilisateur",
|
||||||
|
"icon": "extension",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"directus-extension",
|
||||||
|
"directus-extension-hook"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"directus:extension": {
|
||||||
|
"type": "hook",
|
||||||
|
"path": "dist/index.js",
|
||||||
|
"source": "src/index.js",
|
||||||
|
"host": "^10.10.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "directus-extension build",
|
||||||
|
"dev": "directus-extension build -w --no-minify",
|
||||||
|
"link": "directus-extension link"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@directus/extensions-sdk": "12.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export default ({filter}, {services, env}) => {
|
||||||
|
const checkEmailExists = async (email, database) => {
|
||||||
|
const user = await database('directus_users')
|
||||||
|
.select('id')
|
||||||
|
.where({email})
|
||||||
|
.first()
|
||||||
|
|
||||||
|
return user !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
filter('users.create', async (input, {schema}, {database}) => {
|
||||||
|
if (!services.MailService) {
|
||||||
|
console.error('Le service MailService est manquant.')
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminEmail = env.EMAIL_NEW_USER
|
||||||
|
const directusURL = env.DIRECTUS_URL || 'http://0.0.0.0:8055'
|
||||||
|
|
||||||
|
if (!adminEmail) {
|
||||||
|
console.error('La variable EMAIL_NEW_USER est manquante.')
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailExists = await checkEmailExists(input.email, database)
|
||||||
|
|
||||||
|
if (emailExists) {
|
||||||
|
console.error('L’adresse e-mail est déjà utilisée.')
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mailService = new services.MailService({schema})
|
||||||
|
mailService.send({
|
||||||
|
to: adminEmail,
|
||||||
|
subject: `Nouvel utilisateur : ${input.email}`,
|
||||||
|
text: `Un nouvel utilisateur a été créé :\n Nom: ${input.first_name || 'N/A'} Email: ${input.email || 'N/A'}\n Pour valider => ${directusURL}/admin/users`,
|
||||||
|
html: `
|
||||||
|
<p>Un nouvel utilisateur a été créé :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Nom:</strong> ${input.first_name || 'N/A'} </li>
|
||||||
|
<li><strong>Email:</strong> ${input.email || 'N/A'}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Pour valider, <a href="${directusURL}/admin/users" target="_blank"><strong>cliquez ici</strong></a> ou sur ce lien => <a href="${directusURL}/admin/users" target="_blank">${directusURL}/admin/users</a></p>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Email envoyé avec succès à', adminEmail)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur lors de l’envoi de l’e-mail via MailService:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
})
|
||||||
|
}
|
||||||
Generated
+1112
-275
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "api.konstitisyon.la",
|
"name": "api.konstitisyon.nu",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -12,5 +14,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"directus": "^11.2.1",
|
"directus": "^11.2.1",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {defineConfig} from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: false,
|
||||||
|
environment: 'node',
|
||||||
|
include: ['extensions/*/src/__tests__/**/*.test.js'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user