Compare commits
5 Commits
fe8b3baf1f
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
0751790cbf
|
|||
|
0e56909626
|
|||
|
d59972af91
|
|||
|
154856b2a9
|
|||
|
df3219c3ba
|
+5
-1
@@ -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
|
||||
|
||||
+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
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
directus:
|
||||
image: directus/directus:11.14.0
|
||||
image: directus/directus:11.17.2
|
||||
ports:
|
||||
- 8055:8055
|
||||
volumes:
|
||||
|
||||
@@ -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,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()
|
||||
})
|
||||
})
|
||||
Generated
+1111
-274
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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