11 Commits

Author SHA1 Message Date
Rampeur b8edd76454 Ajout du dossier 'middlewares' 2025-08-13 19:02:35 +02:00
Rampeur 9bce9b714e Mise à jour des 'models' 2025-08-13 18:47:41 +02:00
Rampeur e741081966 Mises à jour relatives à la DB 2025-08-13 01:24:52 +02:00
Rampeur 2f78d85a27 Ajout du dossier 'routes' 2025-08-10 09:16:54 +02:00
Rampeur b68dd4c800 Mise à jour de routes et models 2025-08-10 09:15:18 +02:00
Rampeur 61fdac2434 Mise à jour de models 2025-08-08 20:18:39 +02:00
Rampeur e83b2e831f Ajout d'un fichier index 2025-08-08 19:28:29 +02:00
Rampeur dd5dbbb0fe Ajout du package-lock 2025-08-08 19:23:49 +02:00
Rampeur 16b2de084b Ajout du dossier 'routes' 2025-08-08 19:22:30 +02:00
Rampeur d477cf239c Mise à jour mineure 2025-08-08 19:21:36 +02:00
Rampeur 9a534d5a17 Ajout initial des fichiers de la version mysal 2025-08-08 18:34:15 +02:00
195 changed files with 4950 additions and 33460 deletions
+15
View File
@@ -0,0 +1,15 @@
APP_ENV="development"
NODE_ENV="development"
DEBUG=false
SERVER_PORT="3201"
JWT_SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME="3600"
SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com"
MYSQL_DBNAME="c4adastracomdb2"
MYSQL_DBUSER="c4adastracomu2"
MYSQL_DBPASS="kbrnrt5jPK12"
MYSQL_DBHOST="ns388640.ip-176-31-255.eu"
MYSQL_DBPORT="3306"
MYSQL_DBTYPE="mysql"
+15
View File
@@ -0,0 +1,15 @@
APP_ENV="development"
NODE_ENV="development"
DEBUG=false
SERVER_PORT="3201"
JWT_SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=3600
SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com"
MYSQL_DBNAME="c4adastracomdb2"
MYSQL_DBUSER="c4adastracomu2"
MYSQL_DBPASS="kbrnrt5jPK12"
MYSQL_DBHOST="ns388640.ip-176-31-255.eu"
MYSQL_DBPORT="3306"
MYSQL_DBTYPE="mysql"
+15
View File
@@ -0,0 +1,15 @@
APP_ENV="local"
NODE_ENV="development"
DEBUG=false
SERVER_PORT="3201"
JWT_SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=3600
SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com"
MYSQL_DBNAME="c4adastracomdb2"
MYSQL_DBUSER="c4adastracomu2"
MYSQL_DBPASS="kbrnrt5jPK12"
MYSQL_DBHOST="ns388640.ip-176-31-255.eu"
MYSQL_DBPORT="3306"
MYSQL_DBTYPE="mysql"
+15
View File
@@ -0,0 +1,15 @@
APP_ENV="production"
NODE_ENV="production"
DEBUG=false
SERVER_PORT="3201"
JWT_SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=3600
SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com"
MYSQL_DBNAME="c4adastracomdb2"
MYSQL_DBUSER="c4adastracomu2"
MYSQL_DBPASS="kbrnrt5jPK12"
MYSQL_DBHOST="ns388640.ip-176-31-255.eu"
MYSQL_DBPORT="3306"
MYSQL_DBTYPE="mysql"
-156
View File
@@ -1,156 +0,0 @@
# Instructions pour les agents Copilot / AI
But : fournir à un agent IA les informations essentielles pour être immédiatement productif dans ce workspace.
## Vue d'ensemble des projets
- **adastra_api/** : API Express/Node.js avec Sequelize (MySQL) + Mongoose (Mongo)
- **adastra_app/** : Frontend Angular 17
## Entrées principales de l'application
### API (adastra_api)
- **Point d'entrée :** `app.js` (racine du projet)
- Charge les variables d'environnement via `APP_ENV` (`.env.${APP_ENV}`)
- Initialise les middlewares (CORS, Morgan, session, etc.)
- Connecte MongoDB et MySQL
- Lance le serveur sur le port (défaut : 3200)
- **Routage Express :** `src/app.js` et `src/routes/`
- Toutes les routes API sont sous `/api/v1`
- Exemple : `GET /api/v1/articles``src/controllers/article.controller.js`
- **Contrôleurs :** `src/controllers/`
- Gèrent la logique métier, validations, réponses JSON
- Suivent le pattern : controller → service → utils → database
### Frontend (adastra_app)
- **Point d'entrée :** `src/main.ts` (bootstrap Angular)
- **Configuration :** `angular.json` (builds, configurations, assets)
- **Routes :** `src/app/app.routes.ts`
- **Composants :** `src/app/components/`
- **Styles globaux :** `src/styles/`
## Architecture et patterns clés
### Structure des données
- **Sequelize (MySQL)** : modèles dans `src/database/models/`
- Utilise `sequelize-cli` pour les migrations (si applicable)
- Auto-généré avec `sequelize-auto`
- **Mongoose (MongoDB)** : modèles dans `src/database/models/mongo`
- Initialisé avec `require('./src/database/models/mongo')`
### Gestion des erreurs
Middleware centralisé dans `src/middlewares/exceptions.handler` :
- `notFoundErrorHandler` : capture 404
- `logErrorHandler` : log des erreurs
- `fianlErrorHandler` : réponse finale au client
### Authentification & configuration
- **Passport :** `src/config/passport-local.js`, `passport-headerapikey.js`
- **Config générale :** `src/config/index.js` (secrets, session lifetime, DB)
- **Variables d'environnement :** `.env.${APP_ENV}` (local, development, production)
## Commandes essentielles
### API (adastra_api)
```bash
# Installation
cd adastra_api && npm install
# Développement (avec hot reload via nodemon)
npm run local # APP_ENV=local nodemon ./app.js (port 3200)
# Production/Staging
npm run dev # APP_ENV=development node ./app.js
npm run start # APP_ENV=production node ./app.js
# Tests
npm test # Newman : exécute tests/adastra-api-tests.postman_collection.json
# Arrêter le serveur
npm run stop # Tue le processus sur le port 3200
# MongoDB (Docker)
npm run mongo:start # Lance le conteneur Mongo (port 27017)
npm run mongo:stop # Arrête et nettoie le conteneur
```
### Frontend (adastra_app)
```bash
# Installation
cd adastra_app && npm install
# Développement
npm run local # ng serve --configuration local --port 4400
npm start # ng serve (port 4200 par défaut)
npm run dev # ng serve --configuration local --port 4200
# Build
npm run build # Production build (dist/)
npm run watch # Watch mode
# Linting
npm run lint # ESLint
```
## Conventions et bonnes pratiques
### Ajout d'une nouvelle route API
1. Créer/modifier la route dans `src/routes/` (ex. `src/routes/api/v2/articles.routes.js`)
2. Implémenter le contrôleur dans `src/controllers/article.controller.js`
3. Ajouter validation dans `src/validations/` si nécessaire
4. Tester via Postman → exporter en `tests/adastra-api-tests.postman_collection.json`
### Ajout d'un modèle de données
- **Sequelize :** créer le fichier dans `src/database/models/`, utiliser `sequelize-cli` si migrations
- **Mongoose :** créer le schéma dans `src/database/models/mongo/`, importer dans `src/database/models/mongo/index.js`
### Structure de réponse API
Vérifier `src/controllers/` pour le format des réponses — généralement :
```javascript
res.status(200).json({ data: [...], message: 'Success' });
res.status(400).json({ error: 'Validation failed', details: [...] });
```
## Variables d'environnement essentielles
À définir dans `.env.local`, `.env.development`, `.env.production` :
- `SERVER_PORT` : port du serveur (défaut 3200)
- `APP_ENV` : `local`, `development`, ou `production`
- `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` : configuration MySQL
- `MONGO_URI` : URI de MongoDB
- Clés API externes (SendGrid, etc.)
## Points d'attention
- **Ne pas modifier** directement l'ordre des middlewares dans `app.js` sans tester l'intégralité du flow
- **Versions Node :** respecter `engines` dans `package.json` (`^16.20.2 || ^18.19.1 || ^20.11.1`)
- **Dépendances critiques :** `express`, `sequelize`, `mongoose`, `passport`, `jsonwebtoken`
## Ressources utiles pour un agent
Si tu dois ajouter/modifier du code, consulte d'abord :
1. **Contrôleur similaire existant** : ex. `src/controllers/member.controller.js` pour le pattern
2. **Route similaire existante** : `src/routes/` pour voir comment router
3. **Config/Middleware** : vérifier si logique centralisée existe déjà (`src/middlewares/`, `src/config/`)
4. **Tests Postman** : `tests/adastra-api-tests.postman_collection.json` pour valider les endpoints et `tests/adastra-api-tests.postman_environment.json` pour les variables d'environnement
---
**Questions manquantes ?** Demande-moi les détails sur :
- Structure exacte des schémas Sequelize/Mongoose
- Authentification et système d'autorisations
- Workflows de déploiement ou CI/CD
- Intégration frontend/backend (headers, tokens, etc.)
-9
View File
@@ -59,15 +59,6 @@ Thumbs.db
.scannerwork/
sonar.properties
# TemporaryBackup files
**/*.copy.ts
**/*.copy.scss
**/*.copy.html
# SQL Queries, Dumps and Notes
sql_queries/
sql_dumps/
sql_notes/
# Sequelize Auto Config
sequelize-auto-config.json
-8
View File
@@ -1,8 +0,0 @@
const path = require('path');
module.exports = {
config: path.resolve('./src/database/config', 'db.config.js'),
'models-path': path.resolve('./src/database/models/mysql'),
'seeders-path': path.resolve('./src/database/seeders'),
'migrations-path': path.resolve('./src/database/migrations'),
};
-92
View File
@@ -1,92 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
- `npm run local` — nodemon with `APP_ENV=local`, listens on port **3201** (override via `nodemon.json`). The default port elsewhere is **3200**.
- `npm run dev``APP_ENV=development`, no nodemon
- `npm start``APP_ENV=production`
- `npm stop` — kills whatever is on port 3200
- `npm run setup:api` — full bootstrap: migrations → seed roles → interactive admin user creation → seed articles
- `npm test` — Postman/newman collection (no Jest/unit tests). The `tests/` collection JSONs are the source of truth.
- `npm run create:user` — interactive admin user creation script
- `npm run mongo:start` / `mongo:stop` — local MongoDB via Docker container `hu-mongo` on 27017
Sequelize CLI:
- `npx sequelize-cli db:migrate` / `db:migrate:status` / `db:migrate:undo:all`
- `npx sequelize-cli db:seed:all` or `--seed <name>` for a specific seeder
Paths are redirected by [.sequelizerc](.sequelizerc): config in `src/database/config/db.config.js`, models in `src/database/models/mysql/`, migrations in `src/database/migrations/`, seeders in `src/database/seeders/`.
Node engines: `^16.20.2 || ^18.19.1 || ^20.11.1`. ESM eslint config (`eslint.config.mjs`) but the source code is CommonJS.
## Environments and secrets
`app.js` loads `config/env/.env.${APP_ENV}` at boot — every command must set `APP_ENV` (the npm scripts do). Defaults to `development` if unset.
**Caution:** [nodemon.json](nodemon.json) currently has real credentials in its `env` block (MySQL host/user/password, SendGrid API key, SkydiverID token). Treat it as sensitive — don't commit further changes to it without intent, and never echo or paste those values.
## Boot sequence
[app.js](app.js) is the single entry point. Order matters:
1. Express app is created, CORS, morgan (color-coded by status), JSON/urlencoded body parsers, method-override, static `/public`, session.
2. `errorhandler` middleware is attached **only when `APP_ENV !== 'production'`**.
3. Mongoose models are registered (`require('./src/database/models/mongo')`) and `passport-local` strategy is initialized.
4. Swagger UI is mounted at `/api-docs` (specs assembled from YAML in `src/swagger/`).
5. **`connectAllDb()` runs first** — connects MongoDB then MySQL. The MySQL step calls `showAllTables()` and compares against the model registry; if any expected table is missing the process exits with an error pointing to `db:migrate`. There is **no `sequelize.sync()`** — schema changes must go through migrations.
6. Only after DBs are up are routes registered (`app.use(require('./src/routes'))`). Three exception handlers (`notFoundErrorHandler`, `logErrorHandler`, `fianlErrorHandler` [sic]) are attached after the routes.
If you add a new model, also register it in the appropriate `index.js` (`src/database/models/mysql/index.js` or `mongo/index.js`) and `src/database/mysql.js` (the latter instantiates each model with the shared `sequelize` instance). Otherwise the boot-time table check won't see it and routes that touch it will 500.
## Routing
[src/routes/index.js](src/routes/index.js) mounts everything under `/api`. [src/routes/api/index.js](src/routes/api/index.js) splits that into four domain routers:
- `/api/skydive/*` → [src/routes/api/skydive/](src/routes/api/skydive/) — aeronefs, applications, canopies, dropzones, jumps, qcm (MongoDB-backed)
- `/api/cms/*` → [src/routes/api/cms/](src/routes/api/cms/) — articles, comments, profiles, tags, user (MySQL-backed)
- `/api/ecommerce/*` → [src/routes/api/ecommerce/](src/routes/api/ecommerce/) — product, products (MySQL-backed). Recently moved out of `cms/`.
- `/api/herowars/*` → [src/routes/api/herowars/](src/routes/api/herowars/) — clans, members, herowars (MongoDB-backed)
The `v1/`, `v2/`, `v3/` folders next to these are **legacy backups slated for deletion** — ignore them, do not extend them, and don't reference them in `routes/api/index.js` (the requires are commented out).
Each domain `index.js` mounts its sub-routers and includes a `ValidationError` handler that returns 422 with field-level errors.
## Layered architecture
`route → controller → service → model`. Don't shortcut layers.
- **Routes** ([src/routes/api/](src/routes/api/)) — Express routers, only wire URL + middleware + controller.
- **Controllers** ([src/controllers/](src/controllers/)) — wrapped in `asyncHandler` ([src/middlewares/asyncHandler.js](src/middlewares/asyncHandler.js)) so thrown errors propagate to the exception handlers. They orchestrate: validate input, call services, shape the response. Naming pattern: `xxx.controller.js`.
- **Services** ([src/services/](src/services/)) — split into `mysql/` (Sequelize) and `mongo/` (Mongoose). Exported through [src/services/index.js](src/services/index.js) as `{ArticleService, CommentService, ProductService, TagService, UserService, ClanService, MemberService}`. Static-method classes; this is where DB queries live.
- **Models** ([src/database/models/](src/database/models/)) — `mysql/` are Sequelize model factories (`(sequelize, DataTypes) => sequelize.define(...)`) registered through [src/database/mysql.js](src/database/mysql.js); `mongo/` are Mongoose schemas registered through [src/database/models/mongo/index.js](src/database/models/mongo/index.js). Sequelize associations live in **one place only**: [src/database/relationships/index.js](src/database/relationships/index.js), called once after the table check passes.
## Auth
[src/middlewares/auth.js](src/middlewares/auth.js) exposes:
- `auth.requiredJwt` / `auth.optional``express-jwt` (HS256) reading `Authorization: <Bearer|Token|Basic|Api-Key> <value>`. Decoded payload lands on `req.payload`.
- `auth.required` — dual-mode. If the `Authorization` header prefix matches `process.env.AUTHORIZATION_PREFIX` (i.e. `Api-Key`) it goes through `passport-headerapikey` and sets `req.application` + `req.payload.id = application.author.id`. Otherwise it falls back to JWT. **Use `auth.required` on endpoints that need to accept either user JWTs or third-party API keys.**
JWT secret comes from `process.env.SECRET` (fallback `'acapisecret'`) — same secret used as bcrypt salt for API-key hashing.
## Data stores
Two databases run side-by-side; each domain picks one:
- **MySQL** (Sequelize) — relational data: users/roles, articles/comments/tags/favorites/followers, products/brands/categories/packaging with their xref tables. Charset `utf8mb4`, `freezeTableName: true` (no auto-pluralization), timezone `+02:00`.
- **MongoDB** (Mongoose) — document data: skydive entities (Jump, JumpFile, Aeronef, Canopy, Dropzone, QCM tree), Hero Wars (Clan, Member, ClanRoles), plus a duplicated set of CMS-style models (Article, Comment, Tag, User, Application) that pre-dates the SQL migration.
Migrations are **big-bang**: a single `20251127000000-initial-schema.js` builds the whole MySQL schema. Seeders (roles, articles) are dated and run individually via `--seed`. Don't expect a chain of incremental migrations.
## Swagger
OpenAPI 3 spec assembled at boot in [src/config/swagger.js](src/config/swagger.js) by reading every `*.yaml` under [src/swagger/](src/swagger/) and merging their `paths` and `components.schemas`. To document a new endpoint, add or extend a YAML there — there is no JSDoc-based generation. UI is at `http://localhost:<port>/api-docs`.
## Migration scripts and reports
[scripts/](scripts/) contains `migrate-to-domain-structure.js`, `update-references-post-migration.js`, `finalize-migration.js`, `create-user.js`. The first three relate to the recent restructure into domain folders (cms → ecommerce split). The `MIGRATION_*.md` and `SCRIPTS_MIGRATION_GUIDE.md` files at the repo root document that effort — read them before editing the route layout. They are reference docs, not active runbooks.
## Related service
The Angular frontend lives at `/Users/julien/Sites/adastra_app` and consumes this API via interceptors that prepend `environment.apiBaseUrl`. Frontend feature domains mirror the four backend route groups (`skydive`, `cms`, `ecommerce`, `herowars`); when adding an endpoint, check whether a matching service exists there.
+2 -152
View File
@@ -1,153 +1,3 @@
# AdAstra API
# AdAstra_API
Backend of the AdAstra platform — an Express REST API serving four feature domains: CMS, e-commerce, skydive club management, and Hero Wars guild analytics.
## Tech stack
| Layer | Choice |
|---|---|
| Runtime | Node.js `^16.20.2 \|\| ^18.19.1 \|\| ^20.11.1` |
| Framework | Express 4 |
| MySQL ORM | Sequelize 6 |
| MongoDB ODM | Mongoose 6 |
| Auth | Passport (local strategy) + JWT (express-jwt / jsonwebtoken) |
| Validation | express-validator |
| Security | Helmet, express-rate-limit, CORS |
| Tests | Jest + Supertest, Postman/Newman |
| Dev server | Nodemon |
## Related repository
The frontend lives in [`adastra_app`](../adastra_app) — an Angular 21 standalone app. It calls this API exclusively through relative paths prepended with `environment.apiBaseUrl`.
## Getting started
```bash
npm install
# Copy the example env file for your environment
cp config/env/.env.example config/env/.env.local
# Edit .env.local: MYSQL_DBNAME, MYSQL_DBUSER, MYSQL_DBPASS, SECRET, …
# Initialize the database (migrate + seed roles + create admin user + seed articles)
npm run setup:api
# Start the dev server
npm run local
```
The server listens on `SERVER_PORT` (default **3200**).
## Commands
| Command | Description |
|---|---|
| `npm start` | Production server (`APP_ENV=production`) |
| `npm run dev` | Development server, uses `.env.development` |
| `npm run local` | Local server with Nodemon, uses `.env.local` |
| `npm test` | Jest unit + integration tests |
| `npm run test:postman` | Newman / Postman collection run |
| `npm run create:user` | Interactive admin user creation script |
| `npm run stop` | Kill the process on port 3200 |
## Environment
Environment files live in `config/env/` and are loaded by `dotenv` at startup based on `APP_ENV`:
| File | Used when |
|---|---|
| `.env.local` | `npm run local` |
| `.env.development` | `npm run dev` |
| `.env.production` | `npm start` |
Copy `.env.example` to create a new environment file. Key variables:
```
SERVER_PORT=3200
SECRET= # JWT signing secret
SESSION_SECRET=
MYSQL_DBNAME=
MYSQL_DBUSER=
MYSQL_DBPASS=
MONGODB_URI=
SENDGRID_API_KEY= # optional — email sending
SKYDIVERID_API_URL= # optional — skydiver ID external API
# TRUST_PROXY=1 # uncomment when behind a reverse proxy
```
## Architecture
### Route domains
All routes are mounted under `/api` and split into four domains:
| Domain | Prefix | Resources |
|---|---|---|
| CMS | `/api/cms` | users, profiles, articles, comments, tags |
| E-commerce | `/api/ecommerce` | products |
| Skydive | `/api/skydive` | aeronefs, canopies, dropzones, jumps, pages, QCM, applications |
| Hero Wars | `/api/herowars` | clans, members |
### Middleware pipeline
Requests flow through:
1. **Helmet** — security headers
2. **CORS** — origin whitelist from `ALLOWED_ORIGINS`
3. **Rate limiter**`express-rate-limit`
4. **Body parser** — JSON + URL-encoded
5. **Passport** — initializes local + header-API-key strategies
6. **Route handlers** — validators → `auth` middleware → controller → `asyncHandler`
7. **Error handlers** — 404 catcher, log handler, final error handler
### Auth
- **Session-less JWT**: tokens are signed with `SECRET` and attached by the client as `Authorization: Token <jwt>`.
- `auth.js` middleware verifies the token via `express-jwt` and populates `req.auth`.
- Password storage: PBKDF2-SHA512 with a random 16-byte salt (no bcrypt dependency on the auth path).
### Databases
- **MySQL** (Sequelize): users, roles, skydive entities, e-commerce, CMS content.
- **MongoDB** (Mongoose): Hero Wars guild/clan data and analytics snapshots.
## Database — migrations & seeders
**One-command setup:**
```bash
npm run setup:api
# Runs: db:migrate → seed roles → create:user → seed articles
```
**Manual steps:**
```bash
npx sequelize-cli db:migrate
npx sequelize-cli db:seed --seed 20251126-add-roles.js
npm run create:user
npx sequelize-cli db:seed --seed 20251127-add-articles.js
```
**Migration helpers:**
```bash
npx sequelize-cli db:migrate:status
npx sequelize-cli db:migrate:undo:all
```
**Seeder helpers:**
```bash
npx sequelize-cli db:seed:all
npx sequelize-cli db:seed --seed 20251127-add-articles.js
npx sequelize-cli db:seed:undo:all
npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js
```
## Documentation
Extended references are in [`docs/`](docs/):
| File | Content |
|---|---|
| [DEPLOYMENT.md](docs/DEPLOYMENT.md) | Full VPS production deployment guide (Apache, PM2, SSL) |
| [ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md) | All env variables documented |
| [SWAGGER_GUIDE.md](docs/SWAGGER_GUIDE.md) | API documentation via Swagger UI |
| [GITEA_INSTALL.md](docs/GITEA_INSTALL.md) | Self-hosted Gitea on Debian 11 + ISPConfig |
API pour l'application Ad Astra
-59
View File
@@ -1,59 +0,0 @@
var appEnv = process.env.APP_ENV;
if (appEnv == undefined) {
appEnv = 'development';
}
require('dotenv').config({ path: `config/env/.env.${appEnv}` });
const { promisify } = require('util'),
chalk = require('chalk'),
morgan = require('morgan');
const DateUtilities = require('./src/utils/dateUtilities'),
exceptionHandler = require('./src/middlewares/exceptions.handler'),
connectDb = require('./src/database/connectDb'),
config = require('./src/config'),
createApp = require('./src/createApp');
const app = createApp();
// Morgan status code color config
morgan.token('statusColor', (req, res) => {
const status = (typeof res.headersSent !== 'boolean' ? Boolean(res.header) : res.headersSent)
? res.statusCode
: undefined;
return status >= 500 ? chalk.red(status)
: status >= 429 ? chalk.magenta(status)
: status >= 400 ? chalk.yellow(status)
: status >= 300 ? chalk.cyan(status)
: status >= 200 ? chalk.green(status)
: chalk.underline(status);
});
app.use(morgan('[:date[iso]] ":method :url HTTP/:http-version" :statusColor :res[content-length] ":referrer" ":user-agent" :remote-addr'));
require('./src/database/models/mongo');
config.initAuth();
const setupRoutes = (app) => {
app.use(require('./src/routes'));
app.use(exceptionHandler.notFoundErrorHandler);
app.use(exceptionHandler.logErrorHandler);
app.use(exceptionHandler.fianlErrorHandler);
};
const startServer = async () => {
try {
await connectDb.connectAllDb();
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('All database connections established, relationships loaded'));
setupRoutes(app);
const port = process.env.SERVER_PORT || 3200;
await promisify(app.listen).bind(app)(port);
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green(`${appEnv} API server listening on port ${port}`));
} catch (error) {
console.error(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Failed to start server:'), error);
process.exit(1);
}
};
startServer();
-40
View File
@@ -1,40 +0,0 @@
# Application Configuration
APP_ENV="development"
NODE_ENV="development"
DEBUG=false
SERVER_PORT=3200
SECRET="your-secret-key-here"
SESSION_SECRET="your-session-secret-here"
SESSION_LIFETIME=86400
ALLOWED_ORIGINS="http://localhost:4200"
# Database - MySQL
MYSQL_DBNAME="adastra_dev"
MYSQL_DBUSER="root"
MYSQL_DBPASS=""
MYSQL_DBHOST="localhost"
MYSQL_DBPORT=3306
MYSQL_DBTYPE="mysql"
# Database - MongoDB
MONGODB_URI="mongodb://localhost:27017/adastradb"
# API Keys & External Services
SENDGRID_API_KEY="your-sendgrid-api-key"
SENDGRID_FROM_MAIL="your-email@example.com"
SENDGRID_TO_MAIL="recipient@example.com"
# CouchDB Configuration
COUCHDB_URL="http://localhost:5984"
COUCHDB_DATABASE="your-database"
COUCHDB_DESIGN="your-design"
COUCHDB_CREDENTIAL="base64-encoded-credentials"
# External APIs
SKYDIVERID_API_URL="https://api.example.com"
SKYDIVERID_API_TOKEN="your-api-token"
AUTHORIZATION_PREFIX="Api-Key"
# Reverse proxy hops in front of the API (e.g. 1 for nginx). Omit in dev.
# Required for rate limiting to use the real client IP behind a proxy.
# TRUST_PROXY=1
-5
View File
@@ -1,5 +0,0 @@
# Environment files - keep .example files only
.env*
# Keep example files for documentation
!.env.example
+265
View File
@@ -0,0 +1,265 @@
const { where } = require("sequelize");
const asyncHandler = require("../middlewares/asyncHandler");
const Article = require("../models/Article");
const Tag = require("../models/Tag");
const User = require("../models/User");
const ErrorResponse = require("../util/errorResponse");
const slugify = require("slugify");
const {
appendFollowers,
appendFavorites,
appendTagList,
} = require("../util/helpers");
const includeOptions = [
{
model: Tag,
as: "tagLists",
attributes: ["name"],
through: { attributes: [] },
},
{ model: User, as: "author", attributes: { exclude: ["email", "password"] } },
];
module.exports.getArticles = asyncHandler(async (req, res, next) => {
const { tag, author, favorited, limit = 20, offset = 0 } = req.query;
const { loggedUser } = req;
const searchOptions = {
include: [
{
model: Tag,
as: "tagLists",
attributes: ["name"],
through: { attributes: [] }, // ? this will remove the rows from the join table
// where: tag ? { name: tag } : {},
...(tag && { where: { name: tag } }),
},
{
model: User,
as: "author",
attributes: { exclude: ["password", "email"] },
// where: author ? { username: author } : {},
...(author && { where: { username: author } }),
},
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [["createdAt", "DESC"]],
distinct: true,
};
let articles = { rows: [], count: 0 };
if (favorited) {
const user = await User.findOne({ where: { username: favorited } });
articles.rows = await user.getFavorites(searchOptions);
articles.count = await user.countFavorites();
} else {
articles = await Article.findAndCountAll(searchOptions);
}
for (let article of articles.rows) {
const articleTags = await article.getTagLists();
// const articleTags = article.tagLists;
appendTagList(articleTags, article);
await appendFollowers(loggedUser, article);
await appendFavorites(loggedUser, article);
delete article.dataValues.Favorites;
}
res
.status(200)
.json({ articles: articles.rows, articlesCount: articles.count });
});
module.exports.createArticle = asyncHandler(async (req, res, next) => {
const { loggedUser } = req;
fieldValidation(req.body.article.title, next);
fieldValidation(req.body.article.description, next);
fieldValidation(req.body.article.body, next);
const { title, description, body, tagList } = req.body.article;
const slug = slugify(title, { lower: true });
const slugInDB = await Article.findOne({ where: { slug: slug } });
if (slugInDB) next(new ErrorResponse("Title already exists", 400));
const article = await Article.create({
title: title,
description: description,
body: body,
});
for (const tag of tagList) {
const tagInDB = await Tag.findByPk(tag.trim());
if (tagInDB) {
await article.addTagList(tagInDB);
} else if (tag.length > 2) {
const newTag = await Tag.create({ name: tag.trim() });
await article.addTagList(newTag);
}
}
delete loggedUser.dataValues.token;
article.dataValues.tagList = tagList;
article.setAuthor(loggedUser);
article.dataValues.author = loggedUser;
await appendFollowers(loggedUser, loggedUser);
await appendFavorites(loggedUser, article);
res.status(201).json({ article });
});
module.exports.deleteArticle = asyncHandler(async (req, res, next) => {
const { slug } = req.params;
const { loggedUser } = req;
const article = await Article.findOne({
where: { slug: slug },
include: includeOptions,
});
if (!article) next(new ErrorResponse("Article not found", 404));
if (article.authorId !== loggedUser.id)
return next(new ErrorResponse("Unauthorized", 401));
await article.destroy();
res.status(200).json({ article });
});
module.exports.updateArticle = asyncHandler(async (req, res, next) => {
const { slug } = req.params;
const { loggedUser } = req;
const article = await Article.findOne({
where: { slug: slug },
include: includeOptions,
});
if (!article) next(new ErrorResponse("Article not found", 404));
if (article.authorId !== loggedUser.id)
return next(new ErrorResponse("Unauthorized", 401));
const { title, description, body } = req.body.article;
const slugInDB = await Article.findOne({
where: { slug: slugify(title ? title : article.title, { lower: true }) },
});
if (slugInDB && slugInDB.slug !== slug)
return next(new ErrorResponse("Title already exists", 400));
await article.update({
title: title ? title : article.title,
description: description ? description : article.description,
body: body ? body : article.body,
});
const articleTags = await article.getTagLists();
appendTagList(articleTags, article);
await appendFollowers(loggedUser, article);
await appendFavorites(loggedUser, article);
res.status(200).json({ article });
});
module.exports.articlesFeed = asyncHandler(async (req, res, next) => {
const { loggedUser } = req;
const { limit = 3, offset = 0 } = req.query;
const authors = await loggedUser.getFollowing();
const articles = await Article.findAndCountAll({
include: includeOptions,
limit: parseInt(limit),
offset: offset * limit,
order: [["createdAt", "DESC"]],
where: { authorId: authors.map((author) => author.id) },
distinct: true,
});
for (const article of articles.rows) {
const articleTags = await article.getTagLists();
appendTagList(articleTags, article);
await appendFollowers(loggedUser, article);
await appendFavorites(loggedUser, article);
}
res.json({ articles: articles.rows, articlesCount: articles.count });
});
module.exports.getArticle = asyncHandler(async (req, res, next) => {
const { loggedUser } = req;
const { slug } = req.params;
const article = await Article.findOne({
where: { slug: slug },
include: includeOptions,
});
if (!article) return next(new ErrorResponse("Article not found", 404));
const articleTags = await article.getTagLists();
appendTagList(articleTags, article);
await appendFollowers(loggedUser, article);
await appendFavorites(loggedUser, article);
res.status(200).json({ article });
});
module.exports.addFavoriteArticle = asyncHandler(async (req, res, next) => {
const { loggedUser } = req;
const { slug } = req.params;
const article = await Article.findOne({
where: { slug: slug },
include: includeOptions,
});
if (!article) return next(new ErrorResponse("Article not found", 404));
await loggedUser.addFavorite(article);
const articleTags = await article.getTagLists();
appendTagList(articleTags, article);
await appendFollowers(loggedUser, article);
await appendFavorites(loggedUser, article);
res.status(200).json({ article });
});
module.exports.deleteFavoriteArticle = asyncHandler(async (req, res, next) => {
const { loggedUser } = req;
const { slug } = req.params;
const article = await Article.findOne({
where: { slug: slug },
include: includeOptions,
});
if (!article) return next(new ErrorResponse("Article not found", 404));
await loggedUser.removeFavorite(article);
const articleTags = await article.getTagLists();
appendTagList(articleTags, article);
await appendFollowers(loggedUser, article);
await appendFavorites(loggedUser, article);
res.status(200).json({ article });
});
const fieldValidation = (field, next) => {
if (!field) {
return next(new ErrorResponse(`Missing fields`, 400));
}
};
+120
View File
@@ -0,0 +1,120 @@
const Comment = require("../models/Comment");
const Article = require("../models/Article");
const User = require("../models/User");
const asyncHandler = require("../middlewares/asyncHandler");
const { appendFollowers } = require("../util/helpers");
const ErrorResponse = require("../util/errorResponse");
module.exports.getComments = asyncHandler(async (req, res, next) => {
const { slug } = req.params;
const { loggedUser } = req;
const article = await Article.findOne({
where: { slug: slug },
});
if (!article) return next(new ErrorResponse("Article not found", 404));
const comments = await article.getComments({
include: [
{
model: User,
as: "author",
attributes: ["username", "bio", "image"],
},
],
});
for (let comment of comments) {
await appendFollowers(loggedUser, comment);
}
res.status(200).json({ comments });
});
module.exports.getComment = asyncHandler(async (req, res, next) => {
const { slug, id } = req.params;
const { loggedUser } = req;
const article = await Article.findOne({
where: { slug: slug },
});
if (!article) return next(new ErrorResponse("Article not found", 404));
const comment = await Comment.findOne({
where: { id: id },
include: [
{
model: User,
as: "author",
attributes: ["username", "bio", "image"],
},
],
});
if (!comment) return next(new ErrorResponse("Comment not found", 404));
await appendFollowers(loggedUser, comment);
res.status(200).json({ comment });
});
module.exports.createComment = asyncHandler(async (req, res, next) => {
const { body } = req.body.comment;
const { slug } = req.params;
const { loggedUser } = req;
fieldValidation(body, next);
const article = await Article.findOne({
where: { slug: slug },
include: [
{ model: User, as: "author", attributes: ["username", "bio", "image"] },
],
});
if (!article) return next(new ErrorResponse("Article not found", 404));
const comment = await Comment.create({
body: body,
articleId: article.id,
authorId: loggedUser.id,
});
delete loggedUser.dataValues.token;
await appendFollowers(loggedUser, loggedUser);
comment.dataValues.author = loggedUser;
res.status(201).json({ comment });
});
module.exports.deleteComment = asyncHandler(async (req, res, next) => {
const { slug, id } = req.params;
const { loggedUser } = req;
const article = await Article.findOne({
where: { slug: slug },
});
if (!article) return next(new ErrorResponse("Article not found", 404));
const comment = await Comment.findOne({
where: { id: id },
});
if (!comment) return next(new ErrorResponse("Comment not found", 404));
if (comment.authorId !== loggedUser.id)
return next(new ErrorResponse("Unauthorized", 401));
await comment.destroy();
res.status(200).json({ comment });
});
const fieldValidation = (field, next) => {
if (!field) {
return next(new ErrorResponse(`Missing fields`, 400));
}
};
+80
View File
@@ -0,0 +1,80 @@
const User = require("../models/User");
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../util/errorResponse");
module.exports.getProfile = asyncHandler(async (req, res, next) => {
const username = req.params.username;
const { loggedUser } = req;
const user = await User.findOne({ where: { username: username } });
if (!user) {
return next(new ErrorResponse(`User not found`, 404));
}
let isFollowing = false;
if (loggedUser) {
const followers = await user.getFollowers();
isFollowing = followers.some((user) => user.id === loggedUser.id);
}
const profile = {
username,
firstname: user.firstname,
lastname: user.lastname,
bio: user.bio,
image: user.image,
following: isFollowing,
};
res.status(200).json({ profile });
});
module.exports.followUser = asyncHandler(async (req, res, next) => {
const username = req.params.username;
const { loggedUser } = req;
const userToFollow = await User.findOne({ where: { username: username } });
if (!userToFollow) {
return next(new ErrorResponse(`User not found`, 404));
}
const currentUser = await User.findByPk(loggedUser.id);
await userToFollow.addFollower(currentUser);
const profile = {
username: username,
firstname: userToFollow.dataValues.firstname,
lastname: userToFollow.dataValues.lastname,
bio: userToFollow.dataValues.bio,
image: userToFollow.dataValues.image,
following: true,
};
res.status(200).json({ profile });
});
module.exports.unfollowUser = asyncHandler(async (req, res, next) => {
const username = req.params.username;
const { loggedUser } = req;
const userToUnfollow = await User.findOne({ where: { username: username } });
if (!userToUnfollow) {
return next(new ErrorResponse(`User not found`, 404));
}
const currentUser = await User.findByPk(loggedUser.id);
await userToUnfollow.removeFollower(currentUser);
const profile = {
username: username,
firstname: userToUnfollow.dataValues.firstname,
lastname: userToUnfollow.dataValues.lastname,
bio: userToUnfollow.dataValues.bio,
image: userToUnfollow.dataValues.image,
following: false,
};
res.status(200).json({ profile });
});
+12
View File
@@ -0,0 +1,12 @@
const Tag = require("../models/Tag");
const { appendTagList } = require("../util/helpers");
module.exports.getTags = async (req, res, next) => {
let tags = await Tag.findAll({
attributes: ["name"],
});
tags = tags.map((tag) => tag.name);
res.status(200).json({ tags });
};
+97
View File
@@ -0,0 +1,97 @@
const asyncHandler = require("../middlewares/asyncHandler");
const User = require("../models/User");
const ErrorResponse = require("../util/errorResponse");
const { sign } = require("../util/jwt");
module.exports.createUser = asyncHandler(async (req, res, next) => {
const { email, password, username, firstname, lastname } = req.body.user;
fieldValidation(email, next);
fieldValidation(password, next);
fieldValidation(username, next);
fieldValidation(firstname, next);
fieldValidation(lastname, next);
const user = await User.create({
email: email,
password: password,
username: username,
firstname: firstname,
lastname: lastname,
});
if (user.dataValues.password) {
delete user.dataValues.password;
}
user.dataValues.token = await sign(user);
user.dataValues.bio = null;
user.dataValues.image = null;
res.status(201).json({ user });
});
module.exports.loginUser = asyncHandler(async (req, res, next) => {
const { email, password } = req.body.user;
fieldValidation(email, next);
fieldValidation(password, next);
const user = await User.findOne({
where: {
email: email,
},
});
if (!user) {
return next(new ErrorResponse(`User not found`, 404));
}
const isMatch = user.matchPassword(password);
if (!isMatch) {
return next(new ErrorResponse("Wrong password", 401));
}
delete user.dataValues.password;
user.dataValues.token = await sign(user);
user.dataValues.bio = null;
user.dataValues.image = null;
res.status(200).json({ user });
});
module.exports.getCurrentUser = asyncHandler(async (req, res, next) => {
const { loggedUser } = req;
const user = await User.findByPk(loggedUser.id);
if (!user) {
return next(new ErrorResponse(`User not found`, 404));
}
user.dataValues.token = req.headers.authorization.split(" ")[1];
res.status(200).json({ user });
});
module.exports.updateUser = asyncHandler(async (req, res, next) => {
await User.update(req.body.user, {
where: {
id: req.user.id,
},
});
const user = await User.findByPk(req.user.id);
user.dataValues.token = req.headers.authorization.split(" ")[1];
res.status(200).json({ user });
});
const fieldValidation = (field, next) => {
if (!field) {
return next(new ErrorResponse(`Missing fields`, 400));
}
};
-210
View File
@@ -1,210 +0,0 @@
# Méthodes disponibles sur l'objet `Article` (Sequelize)
Ce document liste toutes les méthodes automatiquement générées par Sequelize pour le modèle `Article` en fonction des associations définies dans `src/database/relationships/index.js`.
---
## Associations Many-to-Many (belongsToMany)
### 1. **Tags** via `TagList`
Association alias: `articleTags`
Clés: `foreignKey: "articleId"`, `otherKey: "tagName"`
```javascript
article.getArticleTags() // Récupère tous les tags
article.setArticleTags([...]) // Définit les tags
article.addArticleTag(tag) // Ajoute un tag
article.addArticleTags([...]) // Ajoute plusieurs tags
article.removeArticleTag(tag) // Retire un tag
article.removeArticleTags([...]) // Retire plusieurs tags
article.countArticleTags() // Compte les tags
article.hasArticleTag(tag) // Vérifie si le tag est lié
article.hasArticleTags([...]) // Vérifie si les tags sont liés
// Alias généré par Sequelize (correspond à belongsToMany):
// Nom de la clé étrangère singularisée + pluralisé = "ArticleTag(s)" => "addTagList", "getTagList", etc.
article.addTagList(tag) // Ajoute un tag via l'alias
article.addTagLists([...]) // Ajoute plusieurs tags via l'alias
article.getTagLists() // Récupère les tags via l'alias
article.setTagLists([...]) // Définit les tags via l'alias
article.removeTagList(tag) // Retire un tag via l'alias
article.removeTagLists([...]) // Retire plusieurs tags via l'alias
article.countTagLists() // Compte les tags via l'alias
article.hasTagList(tag) // Vérifie si le tag est lié via l'alias
article.hasTagLists([...]) // Vérifie si les tags sont liés via l'alias
```
### 2. **Users (Favoris)** via `Favorite`
Association alias: `articleFavoriteUsers`
Clés: `foreignKey: "articleId"`, `otherKey: "userId"`
```javascript
article.getArticleFavoriteUsers() // Récupère les utilisateurs qui ont favorisé
article.setArticleFavoriteUsers([...]) // Définit les utilisateurs
article.addUserIdUser(user) // Ajoute un utilisateur
article.addArticleFavoriteUsers([...]) // Ajoute plusieurs utilisateurs
article.removeUserIdUser(user) // Retire un utilisateur
article.removeArticleFavoriteUsers([...]) // Retire plusieurs utilisateurs
article.countArticleFavoriteUsers() // Compte les utilisateurs
article.hasUserIdUser(user) // Vérifie si l'utilisateur a favorisé
article.hasArticleFavoriteUsers([...]) // Vérifie si les utilisateurs ont favorisé
```
---
## Associations One-to-Many (hasMany)
### 3. **Comments**
Association alias: `comments`
Clé étrangère: `articleId`
```javascript
article.getComments() // Récupère tous les commentaires
article.setComments([...]) // Définit les commentaires
article.addComment(comment) // Crée/ajoute un commentaire
article.addComments([...]) // Ajoute plusieurs commentaires
article.removeComment(comment) // Retire un commentaire
article.removeComments([...]) // Retire plusieurs commentaires
article.countComments() // Compte les commentaires
article.hasComment(comment) // Vérifie si le commentaire existe
article.hasComments([...]) // Vérifie si les commentaires existent
article.createComment({...}) // Crée un nouveau commentaire
```
### 4. **Favorites** (enregistrements de join)
Association alias: `favorites`
Clé étrangère: `articleId`
```javascript
article.getFavorites() // Récupère les enregistrements Favorite
article.setFavorites([...]) // Définit les favoris
article.addFavorite(favorite) // Ajoute un enregistrement Favorite
article.addFavorites([...]) // Ajoute plusieurs enregistrements
article.removeFavorite(favorite) // Retire un enregistrement Favorite
article.removeFavorites([...]) // Retire plusieurs enregistrements
article.countFavorites() // Compte les enregistrements Favorite
article.hasFavorite(favorite) // Vérifie si le Favorite existe
article.hasFavorites([...]) // Vérifie si les Favorites existent
article.createFavorite({...}) // Crée un nouveau Favorite
```
### 5. **TagLists** (enregistrements de join)
Association alias: `tagLists`
Clé étrangère: `articleId`
```javascript
article.getTagLists() // Récupère les enregistrements TagList
article.setTagLists([...]) // Définit les TagList
article.addTagList(tagList) // Ajoute un enregistrement TagList
article.addTagLists([...]) // Ajoute plusieurs enregistrements
article.removeTagList(tagList) // Retire un enregistrement TagList
article.removeTagLists([...]) // Retire plusieurs enregistrements
article.countTagLists() // Compte les enregistrements TagList
article.hasTagList(tagList) // Vérifie si le TagList existe
article.hasTagLists([...]) // Vérifie si les TagLists existent
article.createTagList({...}) // Crée un nouveau TagList
```
---
## Associations Belongs-To (hasOne inverse)
### 6. **Author (User)**
Association alias: `author`
Clé étrangère: `authorId`
```javascript
article.getAuthor() // Récupère l'auteur
article.setAuthor(user) // Définit l'auteur
article.createAuthor({...}) // Crée un nouvel auteur
```
---
## Méthodes personnalisées du modèle
### Méthodes d'instance définies dans `Article.js` :
```javascript
article.toJSONFor() // Retourne un objet JSON formaté
// Format: { id, slug, title, description, body }
```
---
## Hooks (Triggers)
```javascript
Article.beforeValidate() // Déclenché avant la validation
// Génère automatiquement un slug basé sur le titre
// Format: slug(`${article.title}`) + '-' + random
```
---
## Exemples d'utilisation concrets
### Ajouter des tags à un article
```javascript
const article = await Article.findByPk(1);
const tag = await Tag.findByPk('javascript');
await article.addTagList(tag); // Via alias
// ou
await article.addArticleTag(tag); // Via alias généré automatiquement
```
### Récupérer les commentaires d'un article
```javascript
const article = await Article.findByPk(1);
const comments = await article.getComments();
```
### Obtenir les utilisateurs qui ont favorisé cet article
```javascript
const article = await Article.findByPk(1);
const fans = await article.getArticleFavoriteUsers();
```
### Créer un commentaire pour un article
```javascript
const article = await Article.findByPk(1);
const comment = await article.createComment({
body: "Great article!",
authorId: userId
});
```
### Compter les tags
```javascript
const article = await Article.findByPk(1);
const tagCount = await article.countTagLists();
```
---
## Notes importantes
1. **Noms des méthodes générées** : Sequelize génère les noms en fonction du nom de l'association (alias) ou du modèle.
- Si alias est `articleTags``getArticleTags()`, `addArticleTag()`, etc.
- Si alias est `tagLists``getTagLists()`, `addTagList()`, etc.
2. **Singulier/Pluriel** : Les méthodes `add*` acceptent soit un seul élément (singulier) soit un tableau (pluriel).
3. **Paramètres optionnels** : La plupart des méthodes `get*` acceptent un paramètre `options` pour personnaliser la requête :
```javascript
article.getComments({
where: { createdAt: { [Op.gte]: someDate } },
limit: 10,
offset: 0,
order: [['createdAt', 'DESC']]
});
```
4. **Transactions** : Toutes les opérations CRUD peuvent être encapsulées dans une transaction :
```javascript
const transaction = await sequelize.transaction();
await article.addTagList(tag, { transaction });
await transaction.commit();
```
5. **Alias vs Nom généré** : L'alias défini dans la relation détermine le nom des méthodes générées. Dans ce projet, les alias comme `articleTags`, `tagLists` permettent d'appeler `getTagLists()`, `addTagList()`, etc.
-387
View File
@@ -1,387 +0,0 @@
# Guide de mise en production — Adastra
Serveur cible : VPS Debian 11, Apache, MySQL déjà installés.
Stack : Node.js/Express (API) + Angular 18 (frontend).
---
## Table des matières
1. [Prérequis serveur](#1-prérequis-serveur)
2. [Base de données MySQL](#2-base-de-données-mysql)
3. [Base de données MongoDB](#3-base-de-données-mongodb)
4. [Déploiement de l'API](#4-déploiement-de-lapi)
5. [Déploiement du frontend](#5-déploiement-du-frontend)
6. [Configuration Apache](#6-configuration-apache)
7. [HTTPS avec Let's Encrypt](#7-https-avec-lets-encrypt)
8. [PM2 — démarrage automatique](#8-pm2--démarrage-automatique)
9. [Procédure de mise à jour](#9-procédure-de-mise-à-jour)
---
## 1. Prérequis serveur
### Node.js (via nvm — recommandé)
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
nvm alias default 20
node -v # doit afficher v20.x.x
```
### PM2 (gestionnaire de process Node)
```bash
npm install -g pm2
```
### MongoDB
L'API utilise à la fois MySQL et MongoDB. MongoDB n'est pas fourni par les dépôts Debian 11 par défaut.
```bash
# Clé GPG et dépôt MongoDB 7
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/debian bullseye/mongodb-org/7.0 main" \
| tee /etc/apt/sources.list.d/mongodb-org-7.0.list
apt update && apt install -y mongodb-org
systemctl enable --now mongod
mongod --version
```
### Modules Apache nécessaires
```bash
a2enmod proxy proxy_http rewrite headers ssl
systemctl restart apache2
```
---
## 2. Base de données MySQL
MySQL est déjà installé. Créer la base et l'utilisateur dédié.
```sql
-- En tant que root MySQL
CREATE DATABASE adastra_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'adastra'@'localhost' IDENTIFIED BY 'MOT_DE_PASSE_FORT';
GRANT ALL PRIVILEGES ON adastra_prod.* TO 'adastra'@'localhost';
FLUSH PRIVILEGES;
```
Les migrations Sequelize (voir [section 4](#4-déploiement-de-lapi)) créeront le schéma.
---
## 3. Base de données MongoDB
```bash
# Créer la base et l'utilisateur applicatif
mongosh <<'EOF'
use adastradb
db.createUser({
user: "adastra",
pwd: "MOT_DE_PASSE_FORT",
roles: [{ role: "readWrite", db: "adastradb" }]
})
EOF
```
Activer l'authentification MongoDB si elle n'est pas encore activée :
```bash
# /etc/mongod.conf
security:
authorization: enabled
```
```bash
systemctl restart mongod
```
La `MONGODB_URI` dans le `.env` devient alors :
```
MONGODB_URI=mongodb://adastra:MOT_DE_PASSE_FORT@localhost:27017/adastradb
```
---
## 4. Déploiement de l'API
### Récupérer le code
```bash
mkdir -p /var/www
cd /var/www
git clone <url-du-repo> adastra_api
cd adastra_api
git checkout master # ou la branche de production
npm install --omit=dev
```
### Fichier d'environnement
```bash
cp config/env/.env.example config/env/.env.production
nano config/env/.env.production
```
Valeurs à renseigner obligatoirement :
```dotenv
APP_ENV=production
NODE_ENV=production
SERVER_PORT=3200
# Secrets — générer avec :
# node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
SECRET=<64-octets-hex>
SESSION_SECRET=<64-octets-hex>
# MySQL
MYSQL_DBNAME=adastra_prod
MYSQL_DBUSER=adastra
MYSQL_DBPASS=MOT_DE_PASSE_FORT
MYSQL_DBHOST=localhost
MYSQL_DBPORT=3306
MYSQL_DBTYPE=mysql
# MongoDB
MONGODB_URI=mongodb://adastra:MOT_DE_PASSE_FORT@localhost:27017/adastradb
# CORS — domaine exact du frontend (sans slash final)
ALLOWED_ORIGINS=https://adastra.example.com
# Proxy Apache → Node = 1 saut
TRUST_PROXY=1
# Sendgrid (si les emails sont utilisés)
SENDGRID_API_KEY=
SENDGRID_FROM_MAIL=
SENDGRID_TO_MAIL=
```
### Migrations et seeds initiaux
À exécuter **une seule fois** lors du premier déploiement :
```bash
cd /var/www/adastra_api
APP_ENV=production npx sequelize-cli db:migrate
APP_ENV=production npx sequelize-cli db:seed --seed 20251126-add-roles.js
APP_ENV=production npm run create:user # crée le premier compte admin
APP_ENV=production npx sequelize-cli db:seed --seed 20251127-add-articles.js
```
### Démarrer avec PM2
```bash
cd /var/www/adastra_api
pm2 start app.js --name adastra-api
pm2 save
```
Vérifier que l'API répond :
```bash
curl http://localhost:3200/api/health # ou tout endpoint public
```
---
## 5. Déploiement du frontend
Le build se fait **en local** (ou sur un serveur de CI), puis le répertoire `dist/` est transféré sur le serveur.
### Build en local
```bash
cd /chemin/local/adastra_app
npm install
npm run build # production build → dist/adastra_angular/browser/
```
> **Important :** vérifier que `src/environments/environment.ts` pointe vers l'URL de l'API de production avant de builder.
> ```typescript
> apiBaseUrl: 'https://api.adastra.example.com/api'
> ```
### Transfert sur le serveur
```bash
# Depuis la machine locale
rsync -avz --delete dist/adastra_angular/browser/ \
user@serveur:/var/www/adastra_app/
```
Ou avec scp :
```bash
scp -r dist/adastra_angular/browser/* user@serveur:/var/www/adastra_app/
```
### Permissions
```bash
# Sur le serveur
chown -R www-data:www-data /var/www/adastra_app
chmod -R 755 /var/www/adastra_app
```
---
## 6. Configuration Apache
Architecture recommandée : deux sous-domaines séparés.
| Sous-domaine | Rôle |
|---|---|
| `adastra.example.com` | Frontend Angular (fichiers statiques) |
| `api.adastra.example.com` | API Node.js (reverse proxy) |
### VirtualHost frontend — `/etc/apache2/sites-available/adastra-app.conf`
```apache
<VirtualHost *:80>
ServerName adastra.example.com
DocumentRoot /var/www/adastra_app
<Directory /var/www/adastra_app>
Options -Indexes
AllowOverride None
Require all granted
# Angular HTML5 routing — renvoie tout vers index.html
FallbackResource /index.html
</Directory>
# Compression et cache navigateur
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json
</IfModule>
<FilesMatch "\.(js|css|woff2|png|jpg|svg)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
<FilesMatch "index\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</FilesMatch>
ErrorLog ${APACHE_LOG_DIR}/adastra-app-error.log
CustomLog ${APACHE_LOG_DIR}/adastra-app-access.log combined
</VirtualHost>
```
### VirtualHost API — `/etc/apache2/sites-available/adastra-api.conf`
```apache
<VirtualHost *:80>
ServerName api.adastra.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3200/
ProxyPassReverse / http://127.0.0.1:3200/
# Transmet l'IP réelle du client à Node (requis pour TRUST_PROXY=1)
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
RequestHeader set X-Forwarded-Proto "http"
ErrorLog ${APACHE_LOG_DIR}/adastra-api-error.log
CustomLog ${APACHE_LOG_DIR}/adastra-api-access.log combined
</VirtualHost>
```
### Activer les vhosts
```bash
a2ensite adastra-app.conf
a2ensite adastra-api.conf
apache2ctl configtest # doit afficher "Syntax OK"
systemctl reload apache2
```
---
## 7. HTTPS avec Let's Encrypt
```bash
apt install -y certbot python3-certbot-apache
certbot --apache -d adastra.example.com -d api.adastra.example.com
```
Certbot modifie automatiquement les vhosts pour le HTTPS et programme le renouvellement.
Vérifier le renouvellement automatique :
```bash
certbot renew --dry-run
```
Après activation du HTTPS, penser à mettre à jour dans le `.env.production` :
```dotenv
ALLOWED_ORIGINS=https://adastra.example.com
```
Et dans `environment.ts` avant de rebuilder le frontend :
```typescript
apiBaseUrl: 'https://api.adastra.example.com/api'
```
---
## 8. PM2 — démarrage automatique
Pour que l'API redémarre automatiquement après un reboot :
```bash
pm2 startup # affiche une commande à exécuter en root — la copier/coller
pm2 save
```
Commandes PM2 utiles :
```bash
pm2 list # état des process
pm2 logs adastra-api # logs en temps réel
pm2 restart adastra-api # redémarrage manuel
pm2 stop adastra-api # arrêt
```
---
## 9. Procédure de mise à jour
### Mettre à jour l'API
```bash
cd /var/www/adastra_api
git pull origin master
npm install --omit=dev
# Si des migrations ont été ajoutées
APP_ENV=production npx sequelize-cli db:migrate
pm2 restart adastra-api
```
### Mettre à jour le frontend
En local :
```bash
cd /chemin/local/adastra_app
git pull origin master
npm install
npm run build
rsync -avz --delete dist/adastra_angular/browser/ user@serveur:/var/www/adastra_app/
```
Aucun redémarrage Apache nécessaire — les fichiers statiques sont servis directement.
-92
View File
@@ -1,92 +0,0 @@
# Variables d'environnement — Adastra API
Référence pour les variables sensibles ou non-triviales. Pour les valeurs par défaut et la liste exhaustive, voir `config/env/.env.example`.
---
## TRUST_PROXY
**Fichier concerné :** `src/createApp.js`
**Utilisé par :** `express-rate-limit` (IP réelle du client), `req.ip`
### Principe
Indique à Express combien de proxies se trouvent entre internet et le process Node. Sans cette valeur, `req.ip` retourne l'IP du proxy (`127.0.0.1`) au lieu de l'IP du client réel, ce qui rend le rate limiting inefficace (toutes les requêtes semblent venir de la même IP).
### Valeurs selon la topologie
| Environnement | Topologie | Valeur |
|---|---|---|
| Dev local | Node exposé directement, pas de proxy | ne pas setter (variable absente) |
| Prod — serveur seul | nginx → Node sur le même serveur | `1` |
| Prod — cloud avec LB | nginx → load balancer → Node | `2` |
| Confiance totale | Tous les proxies de la chaîne `X-Forwarded-For` | `true` (déconseillé) |
> **Règle générale :** `TRUST_PROXY` = nombre de proxies entre internet et Node.
### Vérification empirique
Ajouter temporairement cette route et appeler `/debug-ip` depuis l'extérieur :
```js
app.get('/debug-ip', (req, res) => {
res.json({
ip: req.ip,
ips: req.ips,
xForwardedFor: req.headers['x-forwarded-for']
});
});
```
- `req.ip` retourne l'IP réelle du client → valeur correcte
- `req.ip` retourne `127.0.0.1` ou l'IP du proxy → augmenter de 1
- `req.ips` est vide malgré un proxy → idem
**Supprimer la route après vérification.**
### Implémentation actuelle
```js
if (process.env.TRUST_PROXY) {
app.set('trust proxy', parseInt(process.env.TRUST_PROXY, 10) || process.env.TRUST_PROXY);
}
```
Le cast `parseInt(...) || process.env.TRUST_PROXY` permet de passer soit un entier (`"1"``1`) soit la chaîne `"true"`.
---
## SECRET
**Fichier concerné :** `src/config/index.js`
**Utilisé par :** signature JWT, HMAC-SHA256 des API keys, secret de session Express
Valeur obligatoire en production — le process refuse de démarrer si elle est absente. Générer avec :
```bash
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
```
---
## SESSION_SECRET
**Fichier concerné :** `src/createApp.js`
**Utilisé par :** `express-session`
Indépendant de `SECRET` pour limiter la surface d'impact si l'un des deux est compromis. Même commande de génération que `SECRET`. Fallback sur `SECRET` si absent (rétrocompatibilité).
---
## ALLOWED_ORIGINS
**Fichier concerné :** `src/createApp.js`
**Utilisé par :** CORS
Liste d'origines autorisées, séparées par des virgules. Si absente ou vide, CORS refuse toutes les origines cross-domain.
```
ALLOWED_ORIGINS=http://localhost:4200,http://localhost:4300
```
En production, indiquer uniquement le domaine du frontend (ex: `https://adastra.example.com`).
-328
View File
@@ -1,328 +0,0 @@
╔═══════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ✅ MIGRATION AUTOMATIQUE RÉUSSIE - Structure Basée Domaines ║
║ ║
║ De: /api/v1, /api/v2, /api/v3 (confus) ║
║ Vers: /api/skydive, /api/cms, /api/herowars (clair) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
📊 STATISTIQUES DE LA MIGRATION
═══════════════════════════════════════════════════════════════════════════════
✓ Fichiers de routes migrés: 19 fichiers
✓ Domaines créés: 3 nouveaux domaines
✓ Fichiers index.js: 4 créés/mis à jour
✓ Documentation: 5 fichiers de rapport
✓ Scripts de migration: 3 scripts npm
✓ Commits Git: 2 commits
✓ Fichiers Swagger mis à jour: 4 fichiers YAML
🎯 STRUCTURE NOUVELLE CRÉÉE
═══════════════════════════════════════════════════════════════════════════════
src/routes/api/
├── skydive/ ← Parachutisme
│ ├── aeronefs.js
│ ├── applications.js
│ ├── canopies.js
│ ├── dropzones.js
│ ├── jumps.js
│ ├── pages.js
│ ├── profiles.js
│ ├── qcm.js
│ └── index.js
├── cms/ ← Gestion de contenu
│ ├── articles.routes.js
│ ├── comments.routes.js
│ ├── product.routes.js
│ ├── products.routes.js
│ ├── profiles.routes.js
│ ├── tags.routes.js
│ ├── user.routes.js
│ └── index.js
└── herowars/ ← Jeu HeroWars
├── clans.routes.js
├── members.routes.js
├── herowars.routes.js
└── index.js
📝 FICHIERS DE DOCUMENTATION CRÉÉS
═══════════════════════════════════════════════════════════════════════════════
1. MIGRATION_REPORT.md
└─ Rapport détaillé: structure ancienne → nouvelle, fichiers migrés
2. MIGRATION_COMPLETE_SUMMARY.md
└─ Résumé exécutif avec instructions de test et frontend
3. POST_MIGRATION_CHECKLIST.md
└─ Liste de vérification manuelle pour validation
4. SCRIPTS_MIGRATION_GUIDE.md
└─ Guide complet d'utilisation des scripts (CE FICHIER)
5. MIGRATION_SWAGGER_TODO.md
└─ Notes sur les mises à jour Swagger/OpenAPI
⚙️ SCRIPTS DE MIGRATION CRÉÉS
═══════════════════════════════════════════════════════════════════════════════
1. scripts/migrate-to-domain-structure.js
├─ Phase 1: Crée les répertoires de domaines
├─ Phase 2: Migre tous les fichiers de routes
├─ Phase 3: Génère les fichiers index.js
├─ Phase 4: Met à jour src/routes/api/index.js
├─ Phase 5: Crée les notes Swagger
├─ Phase 6: Gère les fichiers Swagger
├─ Phase 7: Effectue les opérations Git
├─ Phase 8: Valide la migration
└─ Phase 9: Génère un rapport
[✅ EXÉCUTÉ: commit 9f73ed0]
2. scripts/update-references-post-migration.js
├─ Phase 1: Met à jour les fichiers YAML Swagger
├─ Phase 2: Met à jour la collection/env Postman
├─ Phase 3: Scanne les références v1/v2/v3 restantes
└─ Phase 4: Génère une liste de vérification
[✅ EXÉCUTÉ]
3. scripts/finalize-migration.js
├─ Phase 1: Valide la structure des domaines
├─ Phase 2: Liste les anciens répertoires
├─ Phase 3: Optionnel - Nettoie les v1/v2/v3 (--cleanup)
├─ Phase 4: Optionnel - Teste les endpoints (--test)
└─ Phase 5: Génère un résumé final
[PRÊT À EXÉCUTER - voir instructions ci-dessous]
🚀 PROCHAINES ÉTAPES IMMÉDIATES
═══════════════════════════════════════════════════════════════════════════════
ÉTAPE 1️⃣: VALIDER LA STRUCTURE
────────────────────────────────────────────────────────────────────────────────
$ node scripts/finalize-migration.js
✓ Affichera: "skydive/: ✓ (8 route files)"
✓ Affichera: "cms/: ✓ (7 route files)"
✓ Affichera: "herowars/: ✓ (3 route files)"
ÉTAPE 2️⃣: NETTOYER LES ANCIENS RÉPERTOIRES (OPTIONNEL)
────────────────────────────────────────────────────────────────────────────────
OPTION A - Via script:
$ node scripts/finalize-migration.js --cleanup
OPTION B - Manuellement:
$ rm -rf src/routes/api/v1 src/routes/api/v2 src/routes/api/v3
$ git add -A && git commit -m "chore: remove old v1/v2/v3 directories"
ÉTAPE 3️⃣: TESTER LE SERVEUR
────────────────────────────────────────────────────────────────────────────────
Terminal 1 - Démarrer l'API:
$ npm run local
Terminal 2 - Tester les endpoints:
$ curl http://localhost:3200/api-docs # Swagger UI
$ curl http://localhost:3200/api/skydive/jumps # Skydive
$ curl http://localhost:3200/api/cms/articles # CMS
$ curl http://localhost:3200/api/herowars/clans # HeroWars
ÉTAPE 4️⃣: METTRE À JOUR LE FRONTEND (IMPORTANT!) 🔴
────────────────────────────────────────────────────────────────────────────────
A. Mettre à jour les environnements Angular:
Fichiers à modifier:
- src/environments/environment.ts
- src/environments/environment.development.ts
- src/environments/environment.local.ts
AVANT:
export const environment = {
apiUrl: '/api/v1',
cmsUrl: '/api/v2',
herowarsUrl: '/api/v3',
};
APRÈS:
export const environment = {
apiUrl: '/api/skydive',
cmsUrl: '/api/cms',
herowarsUrl: '/api/herowars',
};
B. Mettre à jour les services Angular:
Cherchez et remplacez les chemins en dur:
sed -i '' "s|'/api/v1/|'/api/skydive/|g" src/**/*.ts
sed -i '' "s|'/api/v2/|'/api/cms/|g" src/**/*.ts
sed -i '' "s|'/api/v3/|'/api/herowars/|g" src/**/*.ts
C. Tester le frontend:
$ npm start
Vérifier dans DevTools que les appels API vont vers /api/skydive/*, /api/cms/*, etc.
ÉTAPE 5️⃣: POUSSER LES CHANGEMENTS
────────────────────────────────────────────────────────────────────────────────
$ git push origin develop
ou
$ git push --all --follow-tags
📋 RÉSUMÉ DES COMMITS GIT
═══════════════════════════════════════════════════════════════════════════════
Commit 1: 9f73ed0
├─ Message: "refactor: restructure API routes from v1/v2/v3 to domain-based..."
├─ Actions:
│ ├─ Créé: src/routes/api/skydive/, cms/, herowars/
│ ├─ Copié: 19 fichiers de routes
│ └─ Mis à jour: src/routes/api/index.js
└─ État: ✅ MERGED
Commit 2: 74df96c
├─ Message: "docs: add migration documentation, finalization scripts, and update Swagger paths"
├─ Actions:
│ ├─ Créé: 4 fichiers de documentation
│ ├─ Créé: 2 scripts de finalisation
│ └─ Mis à jour: 4 fichiers Swagger YAML
└─ État: ✅ CURRENT HEAD
🔄 AVANT/APRÈS - COMPARAISON DES API
═══════════════════════════════════════════════════════════════════════════════
SAUT/PARACHUTISME:
AVANT: GET /api/v1/jumps
APRÈS: GET /api/skydive/jumps
──────────────────────
GET /api/v1/aeronefs → GET /api/skydive/aeronefs
POST /api/v1/aeronefs → POST /api/skydive/aeronefs
CONTENU:
AVANT: GET /api/v2/articles
APRÈS: GET /api/cms/articles
────────────────────────
GET /api/v2/articles/123 → GET /api/cms/articles/123
POST /api/v2/articles → POST /api/cms/articles
GET /api/v2/comments → GET /api/cms/comments
JEU:
AVANT: GET /api/v3/clans
APRÈS: GET /api/herowars/clans
────────────────────────
GET /api/v3/members → GET /api/herowars/members
POST /api/v3/members/123 → POST /api/herowars/members/123
⚠️ IMPORTANT: ROLLBACK EN CAS DE PROBLÈME
═══════════════════════════════════════════════════════════════════════════════
Si vous rencontrez des problèmes:
Option 1 - Peu de changements (rollback 2 commits):
$ git reset --hard HEAD~2
Option 2 - Si vous avez poussé, utiliser revert:
$ git revert 74df96c
$ git revert 9f73ed0
Option 3 - Restaurer depuis le remote:
$ git fetch origin
$ git reset --hard origin/develop
✅ CHECKLIST DE VALIDATION POST-MIGRATION
═══════════════════════════════════════════════════════════════════════════════
Backend (adastra_api):
☐ npm run local - serveur démarre sans erreur
☐ curl http://localhost:3200/api-docs - Swagger UI accessible
☐ Endpoints skydive fonctionnent: GET /api/skydive/jumps
☐ Endpoints CMS fonctionnent: GET /api/cms/articles
☐ Endpoints HeroWars fonctionnent: GET /api/herowars/clans
Frontend (adastra_app):
☐ npm start - app démarre sans erreur
☐ Appels API vont vers les nouveaux chemins
☐ Authentification toujours fonctionnelle
☐ Pages affichent les données correctement
☐ Aucune erreur 404 sur les endpoints
Git:
☐ Commits créés et visibles: git log --oneline -5
☐ Prêt pour: git push origin develop
Documentation:
☐ README.md mis à jour (optionnel)
☐ Équipe notifiée du changement
☐ Documentation technique mise à jour
📚 RESSOURCES SUPPLÉMENTAIRES
═══════════════════════════════════════════════════════════════════════════════
Consultez ces fichiers pour plus d'informations:
1. SCRIPTS_MIGRATION_GUIDE.md
└─ Guide détaillé de chaque script et options
2. POST_MIGRATION_CHECKLIST.md
└─ Liste complète de vérification manuelle
3. MIGRATION_COMPLETE_SUMMARY.md
└─ Résumé exécutif avec instructions
4. docs/*.md (depuis l'audit architectural)
└─ Architecture complète et ADRs
🎓 POINTS CLÉS À RETENIR
═══════════════════════════════════════════════════════════════════════════════
✓ NON-DESTRUCTIF: Les anciens répertoires restent, vous pouvez les nettoyer
progressivement après vérification
✓ AUTOMATISÉ: Scripts Python/Node qui gèrent la plupart des tâches
Swagger, Postman, routes mis à jour automatiquement
⚠️ MANUEL: Frontend DOIT être mis à jour (recherche/remplace)
Tests doivent être revérifiés
Documentation peut nécessiter mise à jour
✓ GIT-SAFE: Chaque étape crée un commit
Rollback facile si problème
Toutes les modifications sont tracées
✓ BÉNÉFICES: Chemins d'API clairs et explicites
Séparation nette des domaines métier
Meilleure architecture et maintenabilité
Préparation pour futures versions d'API
═══════════════════════════════════════════════════════════════════════════════
✨ Migration complétée avec succès! ✨
Prêt à continuer avec les 20 ADRs d'architecture?
(QUICK_START_WEEK1.md pour la Phase 1)
═══════════════════════════════════════════════════════════════════════════════
Date: 24 avril 2026
Statut: ✅ COMPLETE
Prochaine étape: Mise à jour du frontend + tests
-338
View File
@@ -1,338 +0,0 @@
# Installation de Gitea sur Debian 11 avec Apache/ISPConfig
Gitea sera accessible sur `https://git.unespace.com` en reverse proxy devant Apache.
Les repos existants dans `/opt/git/` seront migrés à la fin.
---
## Table des matières
1. [Prérequis Apache](#1-prérequis-apache)
2. [Créer l'utilisateur système Gitea](#2-créer-lutilisateur-système-gitea)
3. [Télécharger et installer le binaire](#3-télécharger-et-installer-le-binaire)
4. [Créer les répertoires et la configuration](#4-créer-les-répertoires-et-la-configuration)
5. [Créer le service systemd](#5-créer-le-service-systemd)
6. [Démarrer Gitea et finaliser l'installation](#6-démarrer-gitea-et-finaliser-linstallation)
7. [Configurer le VHost dans ISPConfig](#7-configurer-le-vhost-dans-ispconfig)
8. [Migrer les repos existants](#8-migrer-les-repos-existants)
9. [Mettre à jour les remotes locaux](#9-mettre-à-jour-les-remotes-locaux)
---
## 1. Prérequis Apache
Activer les modules proxy si ce n'est pas déjà fait :
```bash
a2enmod proxy proxy_http headers
systemctl reload apache2
```
---
## 2. Créer l'utilisateur système Gitea
Gitea a besoin de son propre utilisateur Unix. On le crée **sans home** et **sans shell** pour limiter la surface d'attaque, mais on lui donne accès au répertoire des repos existants :
```bash
adduser --system --shell /bin/bash --gecos 'Gitea' --group --home /opt/gitea gitea
```
Pour que Gitea puisse lire les repos appartenant à l'utilisateur `git`, on ajoute `gitea` au groupe `git` :
```bash
usermod -aG git gitea
```
> Si tes repos dans `/opt/git/` appartiennent à `git:git`, cette étape est nécessaire pour la migration.
---
## 3. Télécharger et installer le binaire
Récupérer la dernière version stable depuis la page des releases Gitea. À adapter avec le numéro de version actuel :
```bash
# Vérifier la dernière version sur https://github.com/go-gitea/gitea/releases
GITEA_VERSION="1.26.1"
wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64
chmod +x /tmp/gitea
mv /tmp/gitea /usr/local/bin/gitea
# Vérifier
gitea --version
```
---
## 4. Créer les répertoires et la configuration
```bash
# Répertoires de données
mkdir -p /opt/gitea/{custom,data,log,repositories}
chown -R gitea:gitea /opt/gitea
chmod -R 750 /opt/gitea
# Répertoire de configuration
mkdir -p /etc/gitea
chown root:gitea /etc/gitea
chmod 770 /etc/gitea
```
Créer le fichier de configuration `/etc/gitea/app.ini` :
```bash
cat > /etc/gitea/app.ini << 'EOF'
APP_NAME = Adastra Git
RUN_USER = gitea
RUN_MODE = prod
WORK_PATH = /opt/gitea
[server]
PROTOCOL = http
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3030
DOMAIN = git.unespace.com
ROOT_URL = https://git.unespace.com/
DISABLE_SSH = false
SSH_PORT = 22
SSH_DOMAIN = git.unespace.com
START_SSH_SERVER = false
[database]
DB_TYPE = sqlite3
PATH = /opt/gitea/data/gitea.db
[repository]
ROOT = /opt/gitea/repositories
[security]
INSTALL_LOCK = false
SECRET_KEY =
INTERNAL_TOKEN =
[log]
ROOT_PATH = /opt/gitea/log
MODE = file
LEVEL = warn
[service]
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
EOF
chown root:gitea /etc/gitea/app.ini
chmod 660 /etc/gitea/app.ini
```
> **Note :** `INSTALL_LOCK = false` permet à l'installeur web de fonctionner au premier démarrage. Il passera à `true` automatiquement après.
> Le port `3030` est choisi pour éviter tout conflit avec l'API Node.js (3200) et d'autres services éventuels.
---
## 5. Créer le service systemd
```bash
cat > /etc/systemd/system/gitea.service << 'EOF'
[Unit]
Description=Gitea (Git with a cup of tea)
After=network.target
[Service]
Type=simple
User=gitea
Group=gitea
WorkingDirectory=/opt/gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
RestartSec=2s
Environment=USER=gitea HOME=/opt/gitea GITEA_WORK_DIR=/opt/gitea
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable gitea
systemctl start gitea
systemctl status gitea
```
Vérifier que Gitea écoute bien sur le bon port :
```bash
ss -tlnp | grep 3030
```
---
## 6. Démarrer Gitea et finaliser l'installation
À ce stade Gitea tourne mais l'interface d'installation web n'est pas encore accessible publiquement (le VHost n'est pas créé). On peut tester depuis le serveur lui-même :
```bash
curl -s http://127.0.0.1:3030 | head -5
# doit retourner du HTML
```
L'installation web sera finalisée **après** la configuration du VHost (étape 7).
---
## 7. Configurer le VHost dans ISPConfig
### 7.1 Créer le site web
Dans ISPConfig → **Sites → Ajouter un site web** :
| Champ | Valeur |
|---|---|
| Domain | `git.unespace.com` |
| Document Root | `/var/www/gitea-placeholder` (peu importe, ne sera pas utilisé) |
| Auto-subdomain | Aucun |
| Redirect type | Laisser vide |
Sauvegarder.
### 7.2 Activer le SSL Let's Encrypt
Dans l'onglet **SSL** du site que tu viens de créer :
- Cocher **SSL**
- Cocher **Let's Encrypt**
- Sauvegarder
ISPConfig demande le certificat automatiquement. Attendre que le statut passe à "actif" (peut prendre 1-2 minutes).
### 7.3 Ajouter les directives reverse proxy
Dans l'onglet **Options** (ou **Directives Apache** selon ta version d'ISPConfig), dans le champ **Apache Directives**, coller :
```apache
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3030/
ProxyPassReverse / http://127.0.0.1:3030/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
```
Répéter l'opération dans le champ **Apache Directives SSL** (le champ équivalent pour le VHost HTTPS) avec les mêmes lignes.
Sauvegarder. ISPConfig recharge Apache automatiquement.
### 7.4 Vérifier
```bash
curl -I https://git.unespace.com
# HTTP/2 200 ou redirection vers l'installeur Gitea
```
### 7.5 Finaliser l'installation Gitea via le navigateur
Ouvre `https://git.unespace.com` dans ton navigateur. L'installeur Gitea s'affiche.
Paramètres recommandés :
| Champ | Valeur |
|---|---|
| Base URL | `https://git.unespace.com/` |
| SSH Server Domain | `git.unespace.com` |
| SSH Port | `22` |
| HTTP Port | `3030` |
| Database | SQLite3 (déjà configuré) |
| Compte administrateur | Remplir nom/email/mot de passe |
Cliquer **Installer Gitea**. L'installeur met à jour `app.ini` et redirige vers la page d'accueil.
### 7.6 Ajouter ta clé SSH publique
Pour pouvoir pousser via SSH, ta clé publique doit être enregistrée dans Gitea.
Depuis ta machine locale, récupère ta clé publique :
```bash
cat ~/.ssh/id_rsa.pub
# ou si tu utilises ed25519 :
cat ~/.ssh/id_ed25519.pub
```
Dans Gitea : **ton avatar → Paramètres → Clés SSH/GPG → Ajouter une clé** — colle le contenu et valide.
---
## 8. Migrer les repos existants
Tes repos actuels sont dans `/opt/git/` sous forme de repos bare (ex. `/opt/git/adastra_api.git`).
### Option A — Via l'interface Gitea (recommandé)
Pour chaque repo :
1. Dans Gitea, créer un nouveau dépôt vide (même nom, sans initialisation)
2. Depuis ta machine locale, ajouter le nouveau remote et pousser :
```bash
cd /chemin/local/adastra_api
git remote add gitea gitea@git.unespace.com:TON_USERNAME/adastra_api.git
git push gitea --all
git push gitea --tags
```
> **Note :** l'utilisateur SSH est `gitea` (le user système sous lequel tourne Gitea), pas `git`.
### Option B — Copie directe côté serveur
Si tu veux éviter de tout repousser depuis le local, tu peux cloner les repos bare existants directement dans le répertoire de Gitea :
```bash
# Pour chaque repo — adapter le nom d'utilisateur Gitea
su - gitea -s /bin/bash
cd /opt/gitea/repositories/TON_USERNAME/
# Clone bare du repo existant
git clone --mirror /opt/git/adastra_api.git adastra_api.git
git clone --mirror /opt/git/adastra_app.git adastra_app.git
```
Puis dans Gitea → Admin panel → **Git Repositories****Git Resync** pour que Gitea indexe les repos copiés.
---
## 9. Mettre à jour les remotes locaux
Une fois les repos migrés, mettre à jour les remotes dans tes clones locaux :
```bash
# Vérifier les remotes actuels
git remote -v
# Remplacer l'ancien remote origin
git remote set-url origin gitea@git.unespace.com:TON_USERNAME/adastra_api.git
git remote set-url origin gitea@git.unespace.com:TON_USERNAME/adastra_app.git
# Vérifier
git remote -v
git fetch origin
```
> L'ancien serveur Git continue de fonctionner en parallèle jusqu'à ce que tu sois certain que tout est migré.
---
## Commandes d'administration utiles
```bash
# Statut du service
systemctl status gitea
# Logs en temps réel
journalctl -u gitea -f
# Mise à jour de Gitea (remplacer le binaire puis redémarrer)
systemctl stop gitea
wget -O /usr/local/bin/gitea https://dl.gitea.com/gitea/NEW_VERSION/gitea-NEW_VERSION-linux-amd64
chmod +x /usr/local/bin/gitea
systemctl start gitea
```
-111
View File
@@ -1,111 +0,0 @@
╔════════════════════════════════════════════════════════════════════════════╗
║ MIGRATION SUCCESSFULLY COMPLETED ║
╚════════════════════════════════════════════════════════════════════════════╝
NEW API STRUCTURE:
├── /api/skydive/* ← Skydiving features (jumps, aeronefs, dropzones, etc.)
├── /api/cms/* ← Content management (articles, tags, comments, etc.)
└── /api/herowars/* ← Game features (clans, members, raids, etc.)
WHAT WAS CHANGED:
✓ Routes directory: v1/v2/v3 → domain-based structure
✓ Route definitions: Updated all index.js files
✓ Swagger YAML: Updated path prefixes to new domains
✓ Postman collection: Updated endpoint URLs
FILES MIGRATED:
✓ Skydive domain (8 route files):
- aeronefs.js, applications.js, canopies.js, dropzones.js
- jumps.js, pages.js, profiles.js, qcm.js
✓ CMS domain (8 route files):
- articles.routes.js, comments.routes.js, product.routes.js
- products.routes.js, profiles.routes.js, tags.routes.js, user.routes.js
✓ Hero Wars domain (3 route files):
- clans.routes.js, members.routes.js, herowars.routes.js
TESTING INSTRUCTIONS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Start the API server:
npm run local
2. In another terminal, verify Swagger is updated:
curl http://localhost:3200/api-docs
3. Test a sample endpoint:
curl http://localhost:3200/api/skydive/jumps
curl http://localhost:3200/api/cms/articles
curl http://localhost:3200/api/herowars/clans
FRONTEND UPDATES REQUIRED:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
In your Angular frontend (adastra_app):
1. Update environment files:
src/environments/environment.ts
src/environments/environment.development.ts
src/environments/environment.local.ts
Replace:
- apiUrl: '/api/v1' → apiUrl: '/api/skydive'
- cmUrl: '/api/v2' → cmUrl: '/api/cms'
- hwUrl: '/api/v3' → hwUrl: '/api/herowars'
2. Update API service calls:
// Before:
http.get('/api/v1/jumps')
http.get('/api/v2/articles')
http.get('/api/v3/clans')
// After:
http.get('/api/skydive/jumps')
http.get('/api/cms/articles')
http.get('/api/herowars/clans')
3. Search & replace in your services:
sed -i 's|/api/v1/|/api/skydive/|g' src/**/*.ts
sed -i 's|/api/v2/|/api/cms/|g' src/**/*.ts
sed -i 's|/api/v3/|/api/herowars/|g' src/**/*.ts
GIT OPERATIONS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
View migration commits:
git log --oneline -5
If needed, view detailed changes:
git show HEAD
Push to remote:
git push origin develop
ROLLBACK (if issues):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
If you need to rollback:
git reset --hard HEAD~2
(Adjust the number of commits based on history)
ADDITIONAL RESOURCES:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Documentation files created:
- MIGRATION_REPORT.md: Detailed migration information
- MIGRATION_SWAGGER_TODO.md: Swagger update notes
- POST_MIGRATION_CHECKLIST.md: Verification checklist
Scripts created:
- scripts/migrate-to-domain-structure.js: Main migration script
- scripts/update-references-post-migration.js: Post-migration updates
- scripts/finalize-migration.js: Cleanup and testing
═════════════════════════════════════════════════════════════════════════════
✓ Migration is complete! Follow the testing instructions above.
═════════════════════════════════════════════════════════════════════════════
-67
View File
@@ -1,67 +0,0 @@
═══════════════════════════════════════════════════════════════════════════════
API STRUCTURE MIGRATION REPORT
═══════════════════════════════════════════════════════════════════════════════
MIGRATION: v1/v2/v3 → Domain-Based (skydive/cms/herowars)
OLD STRUCTURE:
└── routes/api/
├── v1/ (Skydive routes)
├── v2/ (CMS + misc routes)
└── v3/ (Hero Wars routes)
NEW STRUCTURE:
└── routes/api/
├── skydive/
├── cms/
└── herowars/
ROUTE MAPPINGS:
✓ /api/v1/* → /api/skydive/*
- applications, aeronefs, canopies, dropzones, jumps, pages, profiles, qcm
✓ /api/v2/* → /api/cms/*
- user, articles, comments, product, products, profiles, tags
✓ /api/v3/* → /api/herowars/*
- clans, members, herowars (moved from v2)
FILES CREATED/UPDATED:
✓ src/routes/api/skydive/index.js
✓ src/routes/api/cms/index.js
✓ src/routes/api/herowars/index.js
✓ src/routes/api/index.js
✓ All route files copied to appropriate domains
MANUAL STEPS REQUIRED:
1. Update Swagger/OpenAPI documentation:
- Review MIGRATION_SWAGGER_TODO.md
- Update path prefixes in src/swagger/*.yaml files
- Update paths in src/config/swagger.js if needed
2. Update app.js (if direct version references exist):
- Search for 'v1', 'v2', 'v3' strings
- Replace with domain names where appropriate
3. Update tests and client calls:
- Replace /api/v1/ with /api/skydive/
- Replace /api/v2/ with /api/cms/
- Replace /api/v3/ with /api/herowars/
4. Update any documentation:
- README.md API endpoints section
- API documentation and guides
5. Test all endpoints:
- Verify each domain routes work correctly
- Check authentication/authorization still functions
- Validate error handling
OPTIONAL: Old v1/v2/v3 directories can be removed after verification:
rm -rf src/routes/api/v1
rm -rf src/routes/api/v2
rm -rf src/routes/api/v3
═══════════════════════════════════════════════════════════════════════════════
-19
View File
@@ -1,19 +0,0 @@
# Swagger Path Updates Required
The following Swagger YAML files need path updates:
## skydive.yaml
- Change: /api/v1/ → /api/skydive/
- Affected routes: /jumps, /aeronefs, /dropzones, /canopies, /pages, /qcm, /applications
## cms.yaml
- Change: /api/v2/ → /api/cms/
- Affected routes: /articles, /comments, /products, /tags, /profiles, /user
## herowars.yaml
- Change: /api/v3/ → /api/herowars/
- Affected routes: /clans, /members
- Note: Add herowars endpoint from former /api/v2/herowars
Update in: src/config/swagger.js if paths are hardcoded.
-80
View File
@@ -1,80 +0,0 @@
# Post-Migration Update Checklist
## Files Updated by Script ✓
- [x] Swagger YAML files (/api/v1 → /api/skydive, etc.)
- [x] Postman collection and environment files
- [x] API routes structure (new domains created)
## Manual Verification Required
### 1. Test API Endpoints
```bash
npm run local # Start the server
# In another terminal:
curl http://localhost:3200/api-docs # Check Swagger UI
```
### 2. Verify Routes
- [ ] GET /api/skydive/jumps
- [ ] GET /api/cms/articles
- [ ] GET /api/herowars/clans
- [ ] Other domain-specific endpoints
### 3. Frontend Updates Required (if applicable)
- [ ] Update Angular environment files (environment.ts, environment.development.ts, etc.)
- [ ] Replace all API calls from /api/v1, /api/v2, /api/v3 to new domains
- [ ] Update service layer URLs
- [ ] Update HTTP interceptors if needed
### 4. Documentation Updates
- [ ] Update README.md with new API structure
- [ ] Update API documentation (if any)
- [ ] Update deployment/infrastructure docs
- [ ] Update team wiki/documentation
### 5. CI/CD Pipeline (if applicable)
- [ ] Update build/deployment scripts referencing v1/v2/v3
- [ ] Update any health checks pointing to old paths
- [ ] Update monitoring/alerting rules
### 6. Client Applications
- [ ] Update all client libraries/SDKs
- [ ] Update mobile app API endpoints
- [ ] Update third-party integrations
### 7. Database/Caching (if applicable)
- [ ] Clear any cached references to old endpoints
- [ ] Update API documentation in databases
- [ ] Check for hardcoded URLs in configuration
## Git Operations
```bash
# View the migration commits
git log --oneline -5
# If rollback needed:
git revert <commit-hash>
# Push changes
git push origin develop
```
## Optional: Cleanup Old Directories
```bash
# After thorough testing, remove old v1/v2/v3 directories:
rm -rf src/routes/api/v1
rm -rf src/routes/api/v2
rm -rf src/routes/api/v3
git add -A
git commit -m "chore: remove old v1/v2/v3 route directories"
```
## Rollback Procedure
If issues arise, rollback with:
```bash
git reset --hard HEAD~2 # Adjust commit count if needed
```
---
Document generated: 2026-04-23T22:48:52.497Z
-329
View File
@@ -1,329 +0,0 @@
# Migration Scripts - Guide d'utilisation
## Vue d'ensemble
Cette migration restructure votre API Express de `/api/v1` `/api/v2` `/api/v3` (qui représentaient des domaines métier, pas des versions API) vers une architecture basée sur les domaines avec des chemins clairs et explicites.
### Ancien structure (confuse)
```
/api/v1/* → Skydive features
/api/v2/* → CMS features
/api/v3/* → Hero Wars features
```
### Nouvelle structure (claire)
```
/api/skydive/* ← Toutes les fonctionnalités de parachutisme
/api/cms/* ← Gestion de contenu (articles, tags, etc.)
/api/herowars/* ← Gestion du jeu HeroWars (clans, membres, raids)
```
---
## Scripts de migration
### 1. `migrate-to-domain-structure.js` ✓ EXÉCUTÉ
**Rôle:** Crée la nouvelle structure et migre les fichiers
**Statut:** ✅ Completed
- Créé les répertoires `/api/skydive`, `/api/cms`, `/api/herowars`
- Copié tous les fichiers de routes vers les nouveaux emplacements
- Généré les nouveaux fichiers `index.js` pour chaque domaine
- Mis à jour `src/routes/api/index.js` pour utiliser les nouveaux chemins
- Créé un commit Git
**Fichiers modifiés:**
```
✓ src/routes/api/skydive/ (8 fichiers)
✓ src/routes/api/cms/ (8 fichiers)
✓ src/routes/api/herowars/ (3 fichiers)
✓ src/routes/api/index.js (mis à jour)
```
**Rapports générés:**
- `MIGRATION_REPORT.md` - Détails complets de la migration
- `MIGRATION_SWAGGER_TODO.md` - Tâches pour Swagger
---
### 2. `update-references-post-migration.js` ✓ EXÉCUTÉ
**Rôle:** Met à jour les références aux chemins dans la documentation
**Statut:** ✅ Completed
- Mis à jour les fichiers YAML Swagger (`/api/v1/``/api/skydive/`, etc.)
- Mis à jour la collection et l'environnement Postman
- Scanné les fichiers clés pour les références restantes
**Rapports générés:**
- `POST_MIGRATION_CHECKLIST.md` - Liste de vérification manuelle
---
### 3. `finalize-migration.js`
**Rôle:** Valide, nettoie, et teste la migration
**Options disponibles:**
```bash
# Valider la structure sans rien faire
node scripts/finalize-migration.js
# Valider et nettoyer les anciens répertoires
node scripts/finalize-migration.js --cleanup
# Valider et tester les endpoints (nécessite le serveur en cours d'exécution)
node scripts/finalize-migration.js --test
# Mode complet: validation, nettoyage et résumé
node scripts/finalize-migration.js --full
```
**Statut:** ✅ Ready to run
**Rapport généré:**
- `MIGRATION_COMPLETE_SUMMARY.md` - Résumé final avec instructions
---
## Guide étape par étape
### Étape 1: Vérifier la migration ✅
```bash
ls -la src/routes/api/
# Devrait afficher: cms/, herowars/, skydive/, v1/, v2/, v3/, index.js
```
### Étape 2: Valider la structure
```bash
node scripts/finalize-migration.js
```
**Résultat attendu:**
```
✓ skydive/: ✓ (8 route files)
✓ cms/: ✓ (7 route files)
✓ herowars/: ✓ (3 route files)
```
### Étape 3: Nettoyer les anciens répertoires (OPTIONNEL)
```bash
# Option A: Via le script
node scripts/finalize-migration.js --cleanup
# Option B: Manuellement
rm -rf src/routes/api/v1 src/routes/api/v2 src/routes/api/v3
git add -A
git commit -m "chore: remove old v1/v2/v3 route directories"
```
### Étape 4: Mettre à jour le frontend (IMPORTANT)
**Fichiers à modifier dans `adastra_app/src/environments/`:**
#### `environment.ts`
```typescript
// AVANT:
export const environment = {
production: false,
apiUrl: '/api/v1',
cmsUrl: '/api/v2',
herowarsUrl: '/api/v3',
};
// APRÈS:
export const environment = {
production: false,
apiUrl: '/api/skydive',
cmsUrl: '/api/cms',
herowarsUrl: '/api/herowars',
};
```
#### `environment.development.ts` et `environment.local.ts`
Applique les mêmes changements
**Mise à jour des services Angular:**
```typescript
// Dans vos services (par exemple, src/app/services/*.service.ts)
// AVANT:
this.http.get(environment.apiUrl + '/jumps')
this.http.get(environment.cmsUrl + '/articles')
this.http.get(environment.herowarsUrl + '/clans')
// APRÈS:
this.http.get(environment.apiUrl + '/jumps')
this.http.get(environment.cmsUrl + '/articles')
this.http.get(environment.herowarsUrl + '/clans')
// Pas de changement si vous utilisez les variables d'environnement!
// Sinon, cherchez/remplacez les chemins en dur
```
**Automatiser la mise à jour du frontend (macOS/Linux):**
```bash
cd adastra_app
sed -i '' "s|/api/v1|/api/skydive|g" src/**/*.ts
sed -i '' "s|/api/v2|/api/cms|g" src/**/*.ts
sed -i '' "s|/api/v3|/api/herowars|g" src/**/*.ts
```
### Étape 5: Tester les endpoints
**Démarrer le serveur API:**
```bash
cd adastra_api
npm run local
```
**Dans un autre terminal, tester les nouveaux chemins:**
```bash
# Tester Swagger UI
curl http://localhost:3200/api-docs
# Tester les endpoints des domaines
curl http://localhost:3200/api/skydive/jumps
curl http://localhost:3200/api/cms/articles
curl http://localhost:3200/api/herowars/clans
# Avec authentification si nécessaire
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3200/api/skydive/jumps
```
### Étape 6: Tester le frontend
```bash
cd adastra_app
npm start
# Ou avec configuration spécifique
npm run local # ng serve --configuration local
```
Vérifier dans les DevTools que les appels API vont vers les nouveaux chemins.
---
## Statistiques de la migration
| Métrique | Valeur |
|----------|--------|
| Nouveaux domaines créés | 3 (skydive, cms, herowars) |
| Fichiers de routes migrés | 19 |
| Fichiers index.js créés/mis à jour | 4 |
| Fichiers Swagger mis à jour | Oui |
| Fichiers Postman mis à jour | Oui |
| Commits Git créés | 1 (structure) + opt. 1 (nettoyage) |
---
## Fichiers de rapport créés
| Fichier | Contenu |
|---------|---------|
| `MIGRATION_REPORT.md` | Rapport détaillé de la migration |
| `MIGRATION_SWAGGER_TODO.md` | Notes pour les mises à jour Swagger |
| `POST_MIGRATION_CHECKLIST.md` | Liste de vérification manuelle |
| `MIGRATION_COMPLETE_SUMMARY.md` | Résumé final et instructions |
| `SCRIPTS_MIGRATION_GUIDE.md` | Ce fichier |
---
## Procédure de rollback
Si vous devez annuler la migration:
```bash
# Voir l'historique
git log --oneline -10
# Annuler les changements (ajuster le nombre de commits)
git reset --hard HEAD~2
# Ou restaurer une branche spécifique
git reset --hard origin/develop
```
---
## Points clés à retenir
**La migration est non-destructive:**
- Les anciens répertoires v1/v2/v3 existent toujours
- Vous pouvez les supprimer après vérification
**Mises à jour automatisées:**
- Scripts: Oui ✓
- Swagger: Oui ✓
- Postman: Oui ✓
⚠️ **Mises à jour manuelles requises:**
- Frontend Angular (environnement + services)
- Tous les appels API en dur dans le code cliente
- Documentation externe
- Tests d'intégration
**Git-safe:**
- Chaque étape crée un commit
- Rollback facile si problème
---
## Troubleshooting
### Les endpoints retournent 404
- ✓ Vérifier que le serveur a redémarré après la migration
- ✓ Vérifier les chemins dans Swagger UI (http://localhost:3200/api-docs)
- ✓ Vérifier que le backend a bien recompilé (avec nodemon)
### Les routes ne sont pas trouvées
- ✓ Vérifier que `src/routes/api/index.js` a les bonnes références
- ✓ Vérifier que chaque domaine a un `index.js`
- ✓ Vérifier l'ordre des middlewares dans `app.js`
### Erreurs d'authentification
- ✓ Les middlewares d'auth ne sont pas affectés
- ✓ Les tokens JWT restent valides
- ✓ Les headers restent les mêmes
### Le frontend ne peut pas accéder à l'API
- ✓ Vérifier CORS dans `app.js`
- ✓ Vérifier les URLs dans les environnements Angular
- ✓ Vérifier les appels http dans les services
---
## Prochaines étapes
1. ✅ Tester tous les endpoints du backend
2. ✅ Mettre à jour le frontend (environnement + services)
3. ✅ Tester le frontend avec le backend modifié
4. ✅ Mettre à jour la documentation
5. ✅ Nettoyer les anciens répertoires v1/v2/v3 (optionnel)
6. ✅ Déployer en staging/production
---
## Ressources supplémentaires
- Documentation architecture: `ARCHITECTURE_DIAGRAMS.md`
- ADR (Architecture Decision Records): `ADR_INDEX.md`
- Configuration Swagger: `src/config/swagger.js`
- Routes configuration: `src/routes/api/index.js`
---
## Support
Pour des questions ou problèmes:
1. ✓ Consulter los fichiers de rapport (`POST_MIGRATION_CHECKLIST.md`, etc.)
2. ✓ Vérifier les logs du serveur: `npm run local` (output)
3. ✓ Vérifier Swagger UI: http://localhost:3200/api-docs
4. ✓ Exécuter la validation: `node scripts/finalize-migration.js`
---
**Migration effectuée le:** 24 avril 2026
**Version API:** v1/v2/v3 → domain-based
**Status:** ✅ Complete
-762
View File
@@ -1,762 +0,0 @@
# Méthodes disponibles sur les modèles MySQL (Sequelize)
Ce document liste toutes les méthodes automatiquement générées par Sequelize pour chaque modèle en fonction des associations définies dans `src/database/relationships/index.js`.
---
## Table des matières
1. [Article](#article)
2. [Tag](#tag)
3. [User](#user)
4. [Comment](#comment)
5. [Favorite](#favorite)
6. [Follower](#follower)
7. [Product](#product)
8. [Brand](#brand)
9. [Category](#category)
10. [Packaging](#packaging)
11. [TagList](#taglist)
12. [Role](#role)
13. [UserRoleXref](#userroleexref)
14. [ProductCategoryXref](#productcategoryxref)
15. [ProductTagXref](#producttagxref)
---
## Article
### Many-to-Many (belongsToMany)
#### Tags via `TagList`
Alias: `articleTags`
```javascript
article.getArticleTags() // Récupère tous les tags
article.setArticleTags([...]) // Définit les tags
article.addArticleTag(tag) // Ajoute un tag
article.removeArticleTag(tag) // Retire un tag
article.countArticleTags() // Compte les tags
article.hasArticleTag(tag) // Vérifie si le tag est lié
```
#### Users (Favoris) via `Favorite`
Alias: `articleFavoriteUsers` (cascade delete activé)
```javascript
article.getArticleFavoriteUsers() // Récupère les utilisateurs qui ont favorisé
article.setArticleFavoriteUsers([...]) // Définit les utilisateurs
article.addArticleFavoriteUser(user) // Ajoute un utilisateur
article.removeArticleFavoriteUser(user) // Retire un utilisateur
article.countArticleFavoriteUsers() // Compte les utilisateurs
article.hasArticleFavoriteUser(user) // Vérifie si l'utilisateur a favorisé
```
### One-to-Many (hasMany)
#### Comments (cascade delete activé)
```javascript
article.getComments() // Récupère tous les commentaires
article.setComments([...]) // Définit les commentaires
article.addComment(comment) // Ajoute un commentaire
article.removeComment(comment) // Retire un commentaire
article.countComments() // Compte les commentaires
article.hasComment(comment) // Vérifie si le commentaire existe
article.createComment({...}) // Crée un commentaire
```
#### Favorites (cascade delete activé)
```javascript
article.getFavorites() // Récupère les enregistrements Favorite
article.setFavorites([...]) // Définit les favoris
article.addFavorite(favorite) // Ajoute un Favorite
article.removeFavorite(favorite) // Retire un Favorite
article.countFavorites() // Compte les Favorites
article.hasFavorite(favorite) // Vérifie si le Favorite existe
article.createFavorite({...}) // Crée un Favorite
```
#### TagLists (cascade delete activé)
```javascript
article.getTagLists() // Récupère les enregistrements TagList
article.setTagLists([...]) // Définit les TagLists
article.addTagList(tagList) // Ajoute un TagList
article.removeTagList(tagList) // Retire un TagList
article.countTagLists() // Compte les TagLists
article.hasTagList(tagList) // Vérifie si le TagList existe
article.createTagList({...}) // Crée un TagList
```
### Belongs-To
#### Author (User)
```javascript
article.getAuthor() // Récupère l'auteur
article.setAuthor(user) // Définit l'auteur
article.createAuthor({...}) // Crée un auteur
```
### Instance Methods
```javascript
article.toJSONFor() // Formate en JSON
```
---
## Tag
### Many-to-Many (belongsToMany)
#### Articles via `TagList`
Alias: `articleIdArticleTagLists`
```javascript
tag.getArticleIdArticleTagLists() // Récupère les articles
tag.setArticleIdArticleTagLists([...])
tag.addArticleIdArticleTagList(article)
tag.removeArticleIdArticleTagList(article)
tag.countArticleIdArticleTagLists()
tag.hasArticleIdArticleTagList(article)
```
#### Products via `ProductTagXref`
Alias: `productIdProductProductTagXrefs`
```javascript
tag.getProductIdProductProductTagXrefs()
tag.setProductIdProductProductTagXrefs([...])
tag.addProductIdProductProductTagXref(product)
tag.removeProductIdProductProductTagXref(product)
tag.countProductIdProductProductTagXrefs()
tag.hasProductIdProductProductTagXref(product)
```
### One-to-Many (hasMany)
#### TagLists
```javascript
tag.getTagLists() // Récupère les enregistrements TagList
tag.setTagLists([...])
tag.addTagList(tagList)
tag.removeTagList(tagList)
tag.countTagLists()
tag.hasTagList(tagList)
tag.createTagList({...})
```
#### ProductTagXrefs
```javascript
tag.getProductTagXrefs() // Récupère les ProductTagXrefs
tag.setProductTagXrefs([...])
tag.addProductTagXref(xref)
tag.removeProductTagXref(xref)
tag.countProductTagXrefs()
tag.hasProductTagXref(xref)
tag.createProductTagXref({...})
```
### Instance Methods
```javascript
tag.toJSONFor() // Formate en JSON
```
---
## User
### Many-to-Many (belongsToMany)
#### Articles (Favoris) via `Favorite`
Alias: `articleIdArticles`
```javascript
user.getArticleIdArticles() // Récupère les articles favorisés
user.setArticleIdArticles([...])
user.addArticleIdArticle(article)
user.removeArticleIdArticle(article)
user.countArticleIdArticles()
user.hasArticleIdArticle(article)
```
#### Followers (ceux qui vous suivent) via `Follower`
Alias: `followerIdUsers`
```javascript
user.getFollowerIdUsers() // Récupère les followers
user.setFollowerIdUsers([...])
user.addFollowerIdUser(user)
user.removeFollowerIdUser(user)
user.countFollowerIdUsers()
user.hasFollowerIdUser(user)
```
#### Following (que vous suivez) via `Follower`
Alias: `userIdUserFollowers`
```javascript
user.getUserIdUserFollowers() // Récupère ceux que vous suivez
user.setUserIdUserFollowers([...])
user.addUserIdUserFollower(user)
user.removeUserIdUserFollower(user)
user.countUserIdUserFollowers()
user.hasUserIdUserFollower(user)
```
#### Roles via `UserRoleXref`
Alias: `roleIdRoles`
```javascript
user.getRoleIdRoles() // Récupère les rôles
user.setRoleIdRoles([...])
user.addRoleIdRole(role)
user.removeRoleIdRole(role)
user.countRoleIdRoles()
user.hasRoleIdRole(role)
```
### One-to-Many (hasMany)
#### Articles (créés par l'utilisateur)
```javascript
user.getArticles() // Récupère les articles créés
user.setArticles([...])
user.addArticle(article)
user.removeArticle(article)
user.countArticles()
user.hasArticle(article)
user.createArticle({...})
```
#### Comments (créés par l'utilisateur)
```javascript
user.getComments() // Récupère les commentaires créés
user.setComments([...])
user.addComment(comment)
user.removeComment(comment)
user.countComments()
user.hasComment(comment)
user.createComment({...})
```
#### Favorites (enregistrements)
```javascript
user.getFavorites() // Récupère les Favorites
user.setFavorites([...])
user.addFavorite(favorite)
user.removeFavorite(favorite)
user.countFavorites()
user.hasFavorite(favorite)
user.createFavorite({...})
```
#### Followers (enregistrements)
```javascript
user.getFollowers() // Récupère les Followers
user.setFollowers([...])
user.addFollower(follower)
user.removeFollower(follower)
user.countFollowers()
user.hasFollower(follower)
user.createFollower({...})
```
#### FollowerFollowers (ceux qui vous suivent via enregistrement)
```javascript
user.getFollowerFollowers() // Récupère les FollowerFollowers
user.setFollowerFollowers([...])
user.addFollowerFollower(follower)
user.removeFollowerFollower(follower)
user.countFollowerFollowers()
user.hasFollowerFollower(follower)
user.createFollowerFollower({...})
```
#### Products (créés/possédés)
```javascript
user.getProducts() // Récupère les produits
user.setProducts([...])
user.addProduct(product)
user.removeProduct(product)
user.countProducts()
user.hasProduct(product)
user.createProduct({...})
```
#### UserRoleXrefs
```javascript
user.getUserRoleXrefs() // Récupère les UserRoleXrefs
user.setUserRoleXrefs([...])
user.addUserRoleXref(xref)
user.removeUserRoleXref(xref)
user.countUserRoleXrefs()
user.hasUserRoleXref(xref)
user.createUserRoleXref({...})
```
### Instance Methods
```javascript
user.validPassword(password) // Valide le mot de passe
user.setPassword(password) // Définit le mot de passe (hash)
user.generateJWT() // Génère un JWT
user.toAuthJSON() // Retourne objet auth
user.toJSONFor() // Formate en JSON
user.toProfileJSONFor() // Formate profil en JSON
```
---
## Comment
### Belongs-To
#### Article
```javascript
comment.getArticle() // Récupère l'article
comment.setArticle(article) // Définit l'article
comment.createArticle({...}) // Crée un article
```
#### Author (User)
```javascript
comment.getAuthor() // Récupère l'auteur
comment.setAuthor(user) // Définit l'auteur
comment.createAuthor({...}) // Crée un auteur
```
### Instance Methods
```javascript
comment.toJSONFor() // Formate en JSON
```
---
## Favorite
### Belongs-To
#### User
```javascript
favorite.getUser() // Récupère l'utilisateur
favorite.setUser(user) // Définit l'utilisateur
favorite.createUser({...}) // Crée un utilisateur
```
#### Article
```javascript
favorite.getArticle() // Récupère l'article
favorite.setArticle(article) // Définit l'article
favorite.createArticle({...}) // Crée un article
```
### Instance Methods
```javascript
favorite.toJSONFor() // Formate en JSON
```
---
## Follower
### Belongs-To
#### User (l'utilisateur suivi)
```javascript
follower.getUser() // Récupère l'utilisateur suivi
follower.setUser(user) // Définit l'utilisateur suivi
follower.createUser({...}) // Crée un utilisateur
```
#### Follower (l'utilisateur qui suit) - alias 'follower'
```javascript
follower.getFollower() // Récupère celui qui suit
follower.setFollower(user) // Définit celui qui suit
follower.createFollower({...}) // Crée un utilisateur qui suit
```
### Instance Methods
```javascript
follower.toJSONFor() // Formate en JSON
```
---
## Product
### Many-to-Many (belongsToMany)
#### Categories via `ProductCategoryXref`
Alias: `productCategories`
```javascript
product.getProductCategories() // Récupère les catégories
product.setProductCategories([...])
product.addProductCategory(category)
product.removeProductCategory(category)
product.countProductCategories()
product.hasProductCategory(category)
```
#### Tags via `ProductTagXref`
Alias: `productTags`
```javascript
product.getProductTags() // Récupère les tags
product.setProductTags([...])
product.addProductTag(tag)
product.removeProductTag(tag)
product.countProductTags()
product.hasProductTag(tag)
```
### One-to-Many (hasMany)
#### ProductCategoryXrefs
```javascript
product.getProductCategoryXrefs() // Récupère les liens catégories
product.setProductCategoryXrefs([...])
product.addProductCategoryXref(xref)
product.removeProductCategoryXref(xref)
product.countProductCategoryXrefs()
product.hasProductCategoryXref(xref)
product.createProductCategoryXref({...})
```
#### ProductTagXrefs
```javascript
product.getProductTagXrefs() // Récupère les liens tags
product.setProductTagXrefs([...])
product.addProductTagXref(xref)
product.removeProductTagXref(xref)
product.countProductTagXrefs()
product.hasProductTagXref(xref)
product.createProductTagXref({...})
```
### Belongs-To
#### Brand
```javascript
product.getBrand() // Récupère la marque
product.setBrand(brand) // Définit la marque
product.createBrand({...}) // Crée une marque
```
#### Owner (User)
```javascript
product.getOwner() // Récupère le propriétaire
product.setOwner(user) // Définit le propriétaire
product.createOwner({...}) // Crée un propriétaire
```
#### Packaging
```javascript
product.getPackaging() // Récupère l'emballage
product.setPackaging(packaging) // Définit l'emballage
product.createPackaging({...}) // Crée un emballage
```
---
## Brand
### One-to-Many (hasMany)
#### Products
```javascript
brand.getProducts() // Récupère les produits
brand.setProducts([...])
brand.addProduct(product)
brand.removeProduct(product)
brand.countProducts()
brand.hasProduct(product)
brand.createProduct({...})
```
---
## Category
### Many-to-Many (belongsToMany)
#### Products via `ProductCategoryXref`
Alias: `productIdProducts`
```javascript
category.getProductIdProducts() // Récupère les produits
category.setProductIdProducts([...])
category.addProductIdProduct(product)
category.removeProductIdProduct(product)
category.countProductIdProducts()
category.hasProductIdProduct(product)
```
### One-to-Many (hasMany)
#### ProductCategoryXrefs
```javascript
category.getProductCategoryXrefs() // Récupère les liens produits
category.setProductCategoryXrefs([...])
category.addProductCategoryXref(xref)
category.removeProductCategoryXref(xref)
category.countProductCategoryXrefs()
category.hasProductCategoryXref(xref)
category.createProductCategoryXref({...})
```
---
## Packaging
### One-to-Many (hasMany)
#### Products
```javascript
packaging.getProducts() // Récupère les produits
packaging.setProducts([...])
packaging.addProduct(product)
packaging.removeProduct(product)
packaging.countProducts()
packaging.hasProduct(product)
packaging.createProduct({...})
```
---
## TagList
### Belongs-To
#### Article
```javascript
tagList.getArticle() // Récupère l'article
tagList.setArticle(article) // Définit l'article
tagList.createArticle({...}) // Crée un article
```
#### Tag
Alias: `tagNameTag`
```javascript
tagList.getTagNameTag() // Récupère le tag
tagList.setTagNameTag(tag) // Définit le tag
tagList.createTagNameTag({...}) // Crée un tag
```
---
## Role
### Many-to-Many (belongsToMany)
#### Users via `UserRoleXref`
Alias: `userIdUserUserRoleXrefs`
```javascript
role.getUserIdUserUserRoleXrefs() // Récupère les utilisateurs
role.setUserIdUserUserRoleXrefs([...])
role.addUserIdUserUserRoleXref(user)
role.removeUserIdUserUserRoleXref(user)
role.countUserIdUserUserRoleXrefs()
role.hasUserIdUserUserRoleXref(user)
```
### One-to-Many (hasMany)
#### UserRoleXrefs
```javascript
role.getUserRoleXrefs() // Récupère les UserRoleXrefs
role.setUserRoleXrefs([...])
role.addUserRoleXref(xref)
role.removeUserRoleXref(xref)
role.countUserRoleXrefs()
role.hasUserRoleXref(xref)
role.createUserRoleXref({...})
```
---
## UserRoleXref
### Belongs-To
#### User
```javascript
xref.getUser() // Récupère l'utilisateur
xref.setUser(user) // Définit l'utilisateur
xref.createUser({...}) // Crée un utilisateur
```
#### Role
```javascript
xref.getRole() // Récupère le rôle
xref.setRole(role) // Définit le rôle
xref.createRole({...}) // Crée un rôle
```
---
## ProductCategoryXref
### Belongs-To
#### Product
```javascript
xref.getProduct() // Récupère le produit
xref.setProduct(product) // Définit le produit
xref.createProduct({...}) // Crée un produit
```
#### Category
```javascript
xref.getCategory() // Récupère la catégorie
xref.setCategory(category) // Définit la catégorie
xref.createCategory({...}) // Crée une catégorie
```
---
## ProductTagXref
### Belongs-To
#### Product
```javascript
xref.getProduct() // Récupère le produit
xref.setProduct(product) // Définit le produit
xref.createProduct({...}) // Crée un produit
```
#### Tag
Alias: `tagNameTag`
```javascript
xref.getTagNameTag() // Récupère le tag
xref.setTagNameTag(tag) // Définit le tag
xref.createTagNameTag({...}) // Crée un tag
```
---
## Notes générales
1. **Paramètres `options`** : Presque toutes les méthodes `get*()` acceptent un objet `options` pour personnaliser la requête :
```javascript
model.getAssociated({
where: { createdAt: { [Op.gte]: someDate } },
limit: 10,
offset: 0,
order: [['createdAt', 'DESC']]
});
```
2. **Transactions** : Encapsulez les opérations dans une transaction :
```javascript
const transaction = await sequelize.transaction();
await model.addAssociated(associated, { transaction });
await transaction.commit();
```
3. **Eager Loading** : Utilisez `include` pour charger les associations :
```javascript
const article = await Article.findByPk(1, {
include: [
{ model: Tag, as: 'articleTags' },
{ model: User, as: 'author' },
{ model: Comment, as: 'comments' }
]
});
```
4. **Alias vs Nom généré** : Le nom des méthodes dépend de l'alias défini. Exemple :
- Alias `articleTags` → `getArticleTags()`, `addArticleTag()`
- Alias `tagLists` → `getTagLists()`, `addTagList()`
5. **Toutes les méthodes de manipulation** acceptent un paramètre `options` final :
```javascript
await article.addTagList(tag, { transaction, validate: true });
```
---
## Seeders
### Seeder Articles (20251127-add-articles.js)
Ce seeder ajoute automatiquement 3 articles avec du contenu Lorem Ipsum et les associe à des tags d'animaux.
#### Exécution
```bash
# Exécuter tous les seeders
npx sequelize-cli db:seed:all
# Annuler un seeder spécifique
npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js
# Annuler tous les seeders
npx sequelize-cli db:seed:undo:all
```
#### Articles créés
| Titre | Tags | Slug |
|-------|------|------|
| Introduction aux écosystèmes marins | Dolphin, Eagle, Wolf | introduction-aux-ecosystemes-marins |
| Les prédateurs de la savane africaine | Lion, Tiger, Wolf | les-predateurs-de-la-savane-africaine |
| L'intelligence animale au-delà de nos attentes | Eagle, Dolphin, Lion | lintelligence-animale-au-dela-de-nos-attentes |
#### Tags disponibles
- Lion
- Eagle
- Dolphin
- Tiger
- Wolf
#### Fonctionnalités du seeder
- ✅ Crée automatiquement les 5 tags s'ils n'existent pas
- ✅ Génère des slugs uniques pour chaque article
- ✅ Associe 3 tags à chaque article via la table `tagList`
- ✅ Utilise les transactions pour l'intégrité des données
- ✅ Support du rollback via `db:seed:undo`
---
## Migrations et Schéma
### Migration initiale (20251127000000-initial-schema.js)
Crée tous les 15 tables avec les contraintes CASCADE appropriées :
**Tables avec CASCADE delete activé :**
- `article` → `comment` (foreignKey: `articleId`)
- `article` → `favorite` (foreignKey: `articleId`)
- `article` → `tagList` (foreignKey: `articleId`)
- `user` → `article` (foreignKey: `authorId`)
- `user` → `comment` (foreignKey: `authorId`)
- `tag` → `tagList` (foreignKey: `tagName`)
**Exécution**
```bash
# Appliquer les migrations
npx sequelize-cli db:migrate
# Voir le statut des migrations
npx sequelize-cli db:migrate:status
# Annuler les migrations
npx sequelize-cli db:migrate:undo:all
```
---
## Configuration de la base de données
### Variables d'environnement
Le fichier `.env.development` doit contenir :
```
DB_HOST=localhost
DB_USER=root
DB_PASS=password
DB_NAME=adastra
DB_DIALECT=mysql
```
### Validation du schéma au démarrage
L'application valide automatiquement la présence du schéma au démarrage :
- ✅ Si le schéma est absent, l'app échoue avec un message d'erreur
- ✅ Si le schéma est présent, les associations sont créées
- ✅ Message d'aide fourni pour exécuter les migrations manquantes
```bash
# Démarrer l'app (validera le schéma)
npm run local # Développement avec nodemon
npm run dev # Développement
npm start # Production
```
-210
View File
@@ -1,210 +0,0 @@
# Guide Swagger - Documentation OpenAPI pour Adastra API
## Accès à la documentation
Une fois le serveur lancé, accédez à : **http://localhost:3200/api-docs**
## Exemple de documentation pour une route
Voici un exemple de comment documenter une GET route avec des paramètres de query :
```javascript
/**
* @swagger
* /api/v1/articles:
* get:
* summary: Récupère la liste des articles
* description: Retourne une liste paginée de tous les articles
* tags:
* - Articles
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Numéro de page
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: Nombre d'articles par page
* responses:
* 200:
* description: Liste des articles récupérée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* title:
* type: string
* content:
* type: string
* total:
* type: integer
* 400:
* description: Erreur de validation
* 401:
* description: Non authentifié
* 500:
* description: Erreur serveur
*/
router.get('/', async (req, res) => {
// ... logique du contrôleur
});
```
## Exemple avec POST et authentification
```javascript
/**
* @swagger
* /api/v1/articles:
* post:
* summary: Crée un nouvel article
* description: Crée un nouvel article avec les données fournies
* tags:
* - Articles
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - title
* - content
* properties:
* title:
* type: string
* example: "Mon Article"
* content:
* type: string
* example: "Contenu de l'article"
* tags:
* type: array
* items:
* type: string
* example: ["tag1", "tag2"]
* responses:
* 201:
* description: Article créé avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* title:
* type: string
* content:
* type: string
* 400:
* description: Données invalides
* 401:
* description: Non authentifié
*/
router.post('/', authenticate, async (req, res) => {
// ... logique du contrôleur
});
```
## Exemple avec paramètres URL
```javascript
/**
* @swagger
* /api/v1/articles/{id}:
* get:
* summary: Récupère un article par ID
* tags:
* - Articles
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID de l'article
* responses:
* 200:
* description: Article récupéré avec succès
* 404:
* description: Article non trouvé
* 500:
* description: Erreur serveur
*/
router.get('/:id', async (req, res) => {
// ... logique du contrôleur
});
```
## Types de schémas courants
### Objets simples
```javascript
schema: {
type: object
properties:
id: { type: integer }
name: { type: string }
email: { type: string, format: email }
age: { type: integer, minimum: 0 }
isActive: { type: boolean }
}
```
### Arrays
```javascript
schema: {
type: array
items:
type: object
properties:
id: { type: integer }
name: { type: string }
}
```
### Enums
```javascript
schema: {
type: string
enum: ["pending", "approved", "rejected"]
}
```
## Tags disponibles
Les tags disponibles dans la config Swagger sont :
- **Articles** - Gestion des articles
- **Users** - Gestion des utilisateurs
- **Products** - Gestion des produits
- **Clans** - Gestion des clans
- **Hero Wars** - Données Hero Wars
## Instructions générales
1. **Placez les commentaires JSDoc directement au-dessus de la définition de la route**
2. **Utilisez le tag `@swagger`** pour indiquer que c'est de la documentation OpenAPI
3. **Commencez par le chemin d'accès** : `/api/v1/articles`, `/api/v1/articles/{id}`, etc.
4. **Spécifiez la méthode HTTP** : `get`, `post`, `put`, `delete`, `patch`
5. **Incluez toujours** : `summary`, `description`, `tags`, `responses`
6. **Pour les routes protégées**, utilisez `security: [{ bearerAuth: [] }]` ou `security: [{ apiKeyAuth: [] }]`
## Pour plus de détails
Consultez la documentation officielle : https://swagger.io/specification/
---
**Note** : Une fois que vous aurez ajouté les commentaires Swagger à vos routes, redémarrez le serveur et accédez à `/api-docs` pour voir la documentation mise à jour.
@@ -1,49 +0,0 @@
# Use Express.js as the HTTP framework
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The backend serves a JSON REST API consumed by the Angular frontend. Requirements are straightforward: HTTP routing, middleware chaining, JSON body parsing, JWT authentication, and database access. No server-side rendering, real-time features, or heavy framework conventions are needed. Which HTTP framework should be used?
## Considered Options
* Express.js
* Fastify
* NestJS
## Decision Outcome
Chosen option: "Express.js", because it provides full control over middleware order and request lifecycle with minimal abstraction, and is well understood without requiring conventions to be learned.
The application is structured around: `src/routes/api/<domain>/` (route definitions), `src/controllers/` (request/response handling), `src/services/` (business logic), `src/middlewares/` (cross-cutting concerns), and `createApp.js` (application factory separating app creation from server startup for testability).
### Positive Consequences
* Minimal abstraction — full control over middleware order and request lifecycle.
* Large ecosystem with well-understood patterns.
### Negative Consequences
* No convention over configuration — project structure is manually maintained.
* Async error handling requires explicit wrapping (`asyncHandler` middleware) since Express 4 does not catch promise rejections natively. Express 5 (not yet used) handles this automatically.
## Pros and Cons of the Options
### Express.js
* Good, because minimal and flexible — no forced conventions.
* Good, because widely known ecosystem.
* Bad, because no built-in async error handling in v4.
### Fastify
* Good, because faster than Express, built-in schema validation.
* Bad, because less familiar; migration cost not justified for current scale.
### NestJS
* Good, because strong conventions, TypeScript-first, built-in dependency injection.
* Bad, because heavyweight — conventions and abstractions are not needed for a straightforward REST API.
* Bad, because would require migrating the entire codebase.
@@ -1,47 +0,0 @@
# Migrate from MongoDB to MySQL with Sequelize
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The project initially used MongoDB, likely chosen for its flexible schema during early prototyping (evidence: `mongo:start`/`mongo:stop` Docker scripts in `package.json`, `$oid` ObjectId references in original data exports). As the data model stabilised and relational queries became more common (joins between users, jumps, canopies, drop zones), a relational database became a better fit. Should the database be migrated?
## Considered Options
* Migrate to MySQL with Sequelize
* Keep MongoDB
## Decision Outcome
Chosen option: "Migrate to MySQL with Sequelize", because the data model is now stable and relational, and MySQL with Sequelize provides migrations, seeders, and relationship declarations that MongoDB cannot enforce at the database level.
Sequelize provides: model definitions with typed fields, a migration system (`sequelize-cli db:migrate`) for schema versioning, seeders for initial reference data, and relationship declarations (`src/database/relationships/`). The original MongoDB data was exported and re-imported into MySQL via a one-shot migration script (since removed).
### Positive Consequences
* Relational integrity enforced at the database level. Joins are first-class.
* Sequelize migrations provide a reproducible setup path (`npm run setup:api`).
### Negative Consequences
* Less flexible schema than MongoDB — changes require migrations.
## Pros and Cons of the Options
### MySQL with Sequelize
* Good, because enforces relational integrity at the database level.
* Good, because migrations provide a versioned, reproducible schema.
* Bad, because schema changes require explicit migration files.
### Keep MongoDB
* Good, because flexible schema — no migrations needed for model changes.
* Bad, because relational queries (joins) are complex and not first-class.
* Bad, because referential integrity is not enforced at the database level.
## Links
* The one-shot MongoDB → MySQL import script was removed from the frontend codebase (see `importJumps()` removal in `logbook.component.ts`).
* The `mongo:start`/`mongo:stop` npm scripts in `package.json` are legacy artefacts from the MongoDB era and can be removed.
@@ -1,50 +0,0 @@
# Document the REST API with OpenAPI/Swagger
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The API must be understandable and testable without reading source code. How should the API be documented?
## Considered Options
* OpenAPI/Swagger (`swagger-jsdoc` + `swagger-ui-express`)
* Postman collection only
* No documentation
## Decision Outcome
Chosen option: "OpenAPI/Swagger", because it generates living, interactive documentation directly from the source code, eliminating the risk of documentation drift.
`swagger-jsdoc` + `swagger-ui-express` generate and serve an interactive OpenAPI 3.0 UI at a dedicated route. Annotations are written as JSDoc comments directly in route/controller files. The existing Postman collection (`tests/adastra-api-tests.postman_collection.json`) is kept for integration and regression testing via Newman (`npm run test:postman`), complementing rather than replacing Swagger.
### Positive Consequences
* Living documentation — always in sync with the code.
* Interactive UI allows manual endpoint testing without a separate tool.
* OpenAPI spec can be used to generate client types if needed.
### Negative Consequences
* JSDoc annotations add verbosity to route files.
* Annotations must be kept up to date manually — stale annotations are possible if discipline slips.
## Pros and Cons of the Options
### OpenAPI/Swagger
* Good, because interactive UI — testable in the browser without Postman.
* Good, because spec is co-located with the code it describes.
* Bad, because requires discipline to keep annotations accurate.
### Postman collection only
* Good, because already present; useful for regression testing.
* Bad, because not suitable as primary documentation — requires Postman to view.
* Bad, because collection and code can diverge silently.
### No documentation
* Good, because zero maintenance overhead.
* Bad, because API is opaque without reading the source.
@@ -1,46 +0,0 @@
# Organise routes by functional domain
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The API covers four independent functional domains (skydive, cms, ecommerce, herowars), each with distinct models, business logic, and frontend consumers. How should routes be organised to reflect these boundaries?
## Considered Options
* Domain-based organisation (`src/routes/api/<domain>/`)
* Flat structure (all routes at one level)
## Decision Outcome
Chosen option: "Domain-based organisation", because it makes domain boundaries explicit and mirrors the frontend's service structure, making the full-stack data flow traceable.
Routes are grouped under `src/routes/api/<domain>/` with a barrel index (`src/routes/api/index.js`). The domain prefix is reflected in the API path (e.g. `/api/skydive/jumps`, `/api/cms/articles`). Each domain owns its routes, controllers, services, and models independently. This structure mirrors the frontend's domain organisation (see frontend ADR 0011).
### Positive Consequences
* Domain boundaries are explicit and enforced by directory structure.
* Consistent mapping between frontend service paths and backend routes simplifies debugging — a frontend service under `core/services/skydive/` maps to `src/routes/api/skydive/`.
### Negative Consequences
* Cross-domain features (shared auth middleware, user model) live in `src/middlewares/` and `src/database/models/` respectively, outside any domain folder.
## Pros and Cons of the Options
### Domain-based organisation
* Good, because each domain is self-contained and independently navigable.
* Good, because symmetric with the frontend service structure.
* Bad, because shared concerns need a neutral layer outside domain folders.
### Flat structure
* Good, because simpler for very small APIs.
* Bad, because domain boundaries become invisible as the API grows.
## Links
* Legacy `v1/`, `v2/`, `v3/` directories exist alongside `api/` as backup snapshots from earlier iterations — see [ADR 0007](0007-legacy-routes-removal.md) for their planned removal.
* Related to frontend [ADR 0011](../../adastra_app/docs/decisions/0011-feature-domain-organisation.md)
@@ -1,46 +0,0 @@
# Maintain frontend and backend in separate repositories
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The frontend (Angular SPA) and backend (Express API) are distinct deployment units with different runtimes, dependencies, and release cycles. Should they live in the same repository or in separate ones?
## Considered Options
* Separate repositories (`adastra_app` and `adastra_api`)
* Monorepo (managed with Nx or Turborepo)
## Decision Outcome
Chosen option: "Separate repositories", because the two applications have independent dependency trees and deployment lifecycles, and the current scale does not justify the tooling overhead of a monorepo.
Both repositories are treated as a single product during development: the Angular frontend's `CLAUDE.md` declares `adastra_api` as an additional working directory so that cross-repo tasks can be handled in a single session. Tasks that span both (e.g. adding an endpoint and its frontend consumer) are handled together.
### Positive Consequences
* Independent dependency management — `package.json` files don't interfere with each other.
* Simpler per-repo CI/CD pipeline when production deployment is configured.
* Each repo's git history reflects only its own changes.
### Negative Consequences
* No shared type definitions between frontend and backend. API contract changes must be coordinated manually.
* Cross-repo changes require two separate commits. A monorepo would allow atomic cross-boundary commits.
## Pros and Cons of the Options
### Separate repositories
* Good, because independent dependency management and deployment.
* Good, because clean git history per application.
* Bad, because no shared types — API contract drift must be caught manually.
* Bad, because cross-repo changes require coordinated commits in two places.
### Monorepo (Nx or Turborepo)
* Good, because atomic cross-boundary commits and shared type definitions.
* Good, because single place to run all tasks (build, test, lint).
* Bad, because significant tooling overhead not justified at the current scale.
* Bad, because requires migrating both repos and learning monorepo tooling.
-53
View File
@@ -1,53 +0,0 @@
# Use JWT for API authentication
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The API must authenticate requests from the Angular frontend. How should user identity be verified on each request?
## Decision Drivers
* Application is currently single-instance with no horizontal scaling requirement.
* Frontend already uses token-based auth (stores JWT in `localStorage` — see frontend ADR 0005).
* Simplicity of operation is preferred over session infrastructure.
## Considered Options
* JWT (JSON Web Tokens) with `express-jwt`
* Session-based authentication
## Decision Outcome
Chosen option: "JWT", because it is stateless (no session store needed), aligns with the frontend's existing token-based auth flow, and is simpler to operate for a single-instance deployment.
Tokens are issued on successful login (`jsonwebtoken` library, `bcrypt` for password hashing). Incoming requests are validated by `express-jwt`, which populates `req.auth` with the decoded payload. The `src/middlewares/auth.js` middleware wraps `express-jwt` and handles role-based access control (`Admin` role required for protected admin routes). The frontend sends tokens as `Authorization: Token <jwt>`.
### Positive Consequences
* Stateless — no session store needed. Horizontally scalable without sticky sessions.
* Single middleware handles auth for all routes.
### Negative Consequences
* Tokens cannot be invalidated server-side before expiry. Acceptable for this use case (internal application, low revocation risk).
## Pros and Cons of the Options
### JWT
* Good, because stateless — no session store infrastructure.
* Good, because works seamlessly with the frontend's `localStorage`-based token flow.
* Bad, because revocation requires token blacklisting, which adds state.
### Session-based authentication
* Good, because sessions can be invalidated immediately server-side.
* Bad, because requires a session store (Redis or database-backed) — adds infrastructure complexity.
* Bad, because sticky sessions or a shared store are needed in multi-instance deployments.
## Links
* Related to frontend [ADR 0005](../../adastra_app/docs/decisions/0005-jwt-authentication-localstorage.md)
* Security note: passwords are hashed with `bcrypt`. The JWT secret must be kept in environment configuration, never committed.
@@ -1,52 +0,0 @@
# Remove legacy route directories v1, v2, v3
* Status: proposed
* Date: 2026-04-26
## Context and Problem Statement
The `src/routes/` directory contains three legacy subdirectories (`v1/`, `v2/`, `v3/`) alongside the active `api/` directory. These are backup snapshots of earlier API iterations kept during the migration to the current domain-based structure. They are not mounted in the application. Should they be removed?
## Decision Drivers
* Legacy directories create confusion about which routes are active.
* They add noise to `grep` output and IDE navigation.
* Stale logic risks being accidentally referenced.
## Considered Options
* Remove `v1/`, `v2/`, `v3/` after verification
* Keep them indefinitely
## Decision Outcome
Chosen option: "Remove after verification", because the directories serve no runtime purpose and their presence actively harms codebase clarity.
Before deletion, verify: no active code references them (direct imports or dynamic requires) and their content has been superseded by the equivalent routes under `src/routes/api/`. Verification command: `grep -r "routes/v[123]" src/`.
This ADR is marked *proposed* rather than *accepted* until the verification step is completed and the deletion is committed.
### Positive Consequences
* Cleaner repository — only active routes remain visible.
* Eliminates the risk of accidentally referencing stale logic.
### Negative Consequences
* If any code path still references these directories, removal would cause a runtime error. The verification step mitigates this risk.
## Pros and Cons of the Options
### Remove after verification
* Good, because removes dead code that harms navigability.
* Bad, because irreversible without git history — must verify before deleting.
### Keep indefinitely
* Good, because no risk of breakage.
* Bad, because perpetuates confusion about what is active.
## Links
* Related to [ADR 0004](0004-route-organisation-by-domain.md) — domain-based `api/` structure that replaces these legacy directories.
@@ -1,63 +0,0 @@
# Use HMAC-SHA256 for API key hashing
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
API keys must be stored hashed in the database (never in plaintext). Authentication requires looking up the corresponding `Application` record by the hash of the incoming key. This makes the hashing algorithm subject to an unusual constraint: **the hash must be deterministic** so that the same key always produces the same hash and the database lookup works.
## Decision Drivers
* Deterministic hashing is required to enable `findOne({ apikey: hash })` lookups.
* The previous implementation used bcrypt with the JWT secret as a fixed salt — this made hashes deterministic but defeated bcrypt's core security property (random salting).
* API keys are UUID v4 values (128 bits of entropy) — they are not low-entropy secrets like passwords.
* A slow hashing function creates a DoS vector: an attacker flooding the API key auth endpoint forces expensive bcrypt operations on every request.
## Considered Options
* bcrypt with random salt (standard password hashing)
* bcrypt with fixed salt (previous implementation)
* HMAC-SHA256 with the application secret
## Decision Outcome
Chosen option: "HMAC-SHA256", because it is deterministic (lookup by hash works), fast (no DoS surface), and cryptographically appropriate for high-entropy secrets. The security guarantee shifts from computational hardness (bcrypt) to secret confidentiality (HMAC key), which is the correct model for API keys.
Implementation: `crypto.createHmac('sha256', secret).update(key).digest('hex')` in `src/middlewares/auth.js` (key generation) and `src/config/passport-headerapikey.js` (verification). The HMAC key is the application `SECRET` environment variable, shared with JWT signing.
### Positive Consequences
* Database lookup by hash remains possible — no change to the query pattern.
* No DoS vector: HMAC-SHA256 is fast (~microseconds vs bcrypt's ~100ms).
* bcrypt's fixed-salt weakness eliminated.
### Negative Consequences
* Security depends on the `SECRET` environment variable remaining confidential. If it leaks, an attacker can pre-compute hashes for any key. `SECRET` must be rotated and all API keys regenerated if a leak is suspected.
* The same `SECRET` is used for JWT signing and HMAC — a future improvement would be to use a dedicated `APIKEY_SECRET` env var.
## Pros and Cons of the Options
### HMAC-SHA256
* Good, because deterministic — lookup by hash works without additional query logic.
* Good, because fast — no DoS risk on the auth endpoint.
* Good, because appropriate for high-entropy inputs (UUID keys have 128 bits of entropy; bcrypt's brute-force resistance is unnecessary).
* Bad, because security depends on secret confidentiality rather than computational cost.
### bcrypt with random salt
* Good, because industry standard for secrets that require brute-force resistance.
* Bad, because non-deterministic — lookup by hash is impossible without storing additional plaintext identifiers.
### bcrypt with fixed salt (previous implementation)
* Good, because deterministic.
* Bad, because a fixed salt makes all hashes pre-computable for a given salt — defeating bcrypt's main purpose.
* Bad, because bcrypt's slowness creates a DoS surface on the auth endpoint.
## Links
* Replaces the fixed-salt bcrypt approach introduced with `passport-headerapikey` strategy.
* Related to [ADR 0006](0006-jwt-authentication.md) — both auth mechanisms share the `SECRET` env var.
@@ -1,58 +0,0 @@
# Migrate Application model from MongoDB to MySQL
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The `Application` model (API key management) was stored in MongoDB while user identity (`User` model, JWT auth) was stored in MySQL. This split the authentication layer across two databases: verifying who a request comes from required MongoDB for API key auth and MySQL for JWT auth. The two-database architecture is otherwise intentional (see [ADR 0002](0002-database-migration-mongodb-to-mysql.md)) but the auth layer is a special case where consistency matters for security auditability and operational simplicity.
## Decision Drivers
* Auth-related models should be co-located to allow transactional integrity (e.g. cascading deletes when a user is removed).
* Security audits are easier when all auth data lives in one queryable store.
* `Application.authorId` references a MySQL `User.id` — a foreign key that MongoDB cannot enforce.
* The skydive and Hero Wars domains intentionally remain on MongoDB; `Application` is not part of those domains.
## Considered Options
* Keep `Application` on MongoDB
* Migrate `Application` to MySQL
## Decision Outcome
Chosen option: "Migrate Application to MySQL", because the auth layer (JWT + API key) is now consolidated on a single database with enforced referential integrity. The skydive and Hero Wars MongoDB collections are unaffected.
Migration implemented via Sequelize migration `20260426000000-add-application.js`. The Mongoose model was deleted and replaced with a Sequelize model in `src/database/models/mysql/Application.js`. The `User hasMany Application` association is declared in `src/database/relationships/index.js`.
### Positive Consequences
* Auth layer fully on MySQL — a single connection pool and query interface for all auth operations.
* `authorId` foreign key enforced at the database level (`ON DELETE SET NULL`).
* Consistent UUID v4 primary keys across all auth models.
* API key hashing algorithm change (ADR 0008) applied cleanly on the new model.
### Negative Consequences
* Existing API key hashes stored in MongoDB are invalidated by the migration (different hashing algorithm). Applications must be recreated via the app to generate new keys.
* Requires a Sequelize migration to create the `application` table before deploying.
## Pros and Cons of the Options
### Keep Application on MongoDB
* Good, because no migration effort.
* Bad, because auth data is split across two databases — harder to audit and no referential integrity with `User`.
* Bad, because `authorId` is an unenforceable soft reference to a MySQL row.
### Migrate Application to MySQL
* Good, because auth layer is consolidated — simpler to audit and operate.
* Good, because foreign key to `User` can be enforced.
* Good, because consistent primary key format (UUID v4) across all MySQL models.
* Bad, because existing MongoDB API key records are invalidated and must be recreated.
## Links
* Related to [ADR 0002](0002-database-migration-mongodb-to-mysql.md) — general migration strategy (MongoDB → MySQL for CMS/ecommerce domains).
* Related to [ADR 0008](0008-hmac-sha256-api-key-hashing.md) — API key hashing algorithm applied on this model.
@@ -1,52 +0,0 @@
# Restrict CORS to explicit allowed origins
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
The API serves an Angular frontend from a different origin. Cross-Origin Resource Sharing (CORS) must be configured to allow the frontend to call the API. The initial configuration used `cors()` with no options, which sets `Access-Control-Allow-Origin: *` and allows any website to make cross-origin requests to the API — including requests carrying a victim's JWT token.
## Decision Drivers
* Any malicious website could make authenticated requests to the API on behalf of a logged-in user (CSRF-like attack via CORS misconfiguration).
* The list of legitimate frontend origins is finite and known at deployment time.
* The `Authorization: Token <jwt>` header is a non-simple header that always triggers a CORS preflight — the server controls which origins receive a valid preflight response.
## Considered Options
* Wildcard CORS (`Access-Control-Allow-Origin: *`)
* Explicit origin allowlist via environment variable
## Decision Outcome
Chosen option: "Explicit origin allowlist", because it limits cross-origin access to known, trusted frontends. Unknown origins receive no CORS headers and are blocked by the browser.
Allowed origins are read from the `ALLOWED_ORIGINS` environment variable as a comma-separated list. If the variable is not set, all cross-origin requests are rejected (`origin: false`). `credentials: true` is set to allow the `Authorization` header to be sent cross-origin.
### Positive Consequences
* Malicious third-party sites cannot make authenticated cross-origin requests on behalf of users.
* Adding a new frontend origin (e.g. a mobile web app) requires only an env var change, no code deployment.
### Negative Consequences
* `ALLOWED_ORIGINS` must be set correctly in every environment (dev, staging, prod) or the frontend will fail with CORS errors. Incorrect configuration is a common operational mistake.
## Pros and Cons of the Options
### Wildcard CORS
* Good, because no configuration required.
* Bad, because allows any origin — malicious sites can make authenticated requests on behalf of logged-in users.
* Bad, because incompatible with `credentials: true` per the CORS spec when `Access-Control-Allow-Origin` is `*`.
### Explicit origin allowlist
* Good, because only known frontends can make cross-origin requests.
* Good, because configurable per environment without code changes.
* Bad, because requires correct `ALLOWED_ORIGINS` configuration in each environment.
## Links
* Related to [ADR 0004](0004-route-organisation-by-domain.md) — all API routes are under `/api/*`, making CORS the only cross-origin entry point.
@@ -1,60 +0,0 @@
# Apply rate limiting to authentication endpoints
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
Authentication endpoints (`POST /login`, `POST /` registration) were exposed without any request throttling. An attacker could perform unlimited credential stuffing, dictionary attacks, or account enumeration against these endpoints with no server-side resistance.
## Decision Drivers
* Login and registration are the highest-value targets for automated attacks.
* No existing infrastructure (WAF, reverse-proxy rate limiting) in front of the API provides this protection at the application layer.
* The rate limit must be permissive enough not to affect legitimate users while blocking automated attack patterns.
## Considered Options
* No rate limiting (previous state)
* Global rate limiting on all routes
* Targeted rate limiting on authentication endpoints only
## Decision Outcome
Chosen option: "Targeted rate limiting on authentication endpoints", because it provides strong protection where it matters most without risking false positives on data-intensive routes (e.g. Hero Wars analytics endpoints that may legitimately send many requests in a short window).
Implementation: `express-rate-limit` middleware with a 10-request / 15-minute window per IP, applied to `POST /api/cms/user/login` and `POST /api/cms/user/` (registration). Returns HTTP 429 with a structured error body on limit exceeded.
### Positive Consequences
* Brute force and credential stuffing attacks are throttled to 10 attempts per 15 minutes per IP.
* Legitimate users (at most a few login attempts per session) are unaffected.
### Negative Consequences
* IP-based rate limiting can be bypassed by attackers rotating IPs or using proxies.
* Shared NAT environments (office networks, VPNs) could hit the limit if multiple users attempt to log in simultaneously from the same IP. The 10-request window is generous enough to make this unlikely in practice.
* If the API is ever placed behind a reverse proxy, `trust proxy` must be configured in Express so that the correct client IP is used rather than the proxy IP.
## Pros and Cons of the Options
### No rate limiting
* Good, because no false positives.
* Bad, because unlimited brute force attacks possible.
### Global rate limiting
* Good, because protects all endpoints uniformly.
* Bad, because risks throttling legitimate use of analytics or data sync endpoints.
### Targeted rate limiting on auth endpoints
* Good, because protects the highest-risk endpoints without affecting others.
* Good, because easy to tune per endpoint independently.
* Bad, because other endpoints (e.g. password reset, if added later) must be manually included.
## Links
* Related to [ADR 0006](0006-jwt-authentication.md) — rate limiting protects the JWT issuance endpoint.
* Related to [ADR 0008](0008-hmac-sha256-api-key-hashing.md) — API key auth endpoint is protected by the deterministic hash lookup rather than rate limiting.
@@ -1,64 +0,0 @@
# Use express-validator for input validation at the route level
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
API endpoints accepted user input without validating format, length, or type. A `fieldValidation` helper existed in several controllers but only checked for empty values, was duplicated across three controllers (users, articles, products), and had a critical bug: it called `next(error)` without `return`, so execution continued even when a field was invalid.
## Decision Drivers
* Input validation must happen before business logic and must halt execution on failure.
* Validation rules should be declared at the route level, not buried inside controller logic.
* The solution must be consistent and reusable across the codebase without adding a framework wrapper.
## Considered Options
* Keep and fix the `fieldValidation` helper
* Joi with a validation middleware wrapper
* express-validator with route-level chains
## Decision Outcome
Chosen option: "express-validator", because it integrates natively as Express middleware, allows validation rules to be declared directly in route definitions, and requires no wrapper function. A shared `validate.js` middleware reads `validationResult` and returns HTTP 422 with a structured error body if any rule fails.
Structure:
- `src/middlewares/validate.js` — shared middleware that checks `validationResult` and short-circuits on errors
- `src/middlewares/validators/users.validators.js` — rule sets for user endpoints
- `src/middlewares/validators/articles.validators.js` — rule sets for article endpoints
- Rules applied at route level: `router.post('/', rules, validate, controller)`
Initial coverage: CMS user endpoints (register, login, update profile/email/username/role) and article endpoints (create, update). Products and tags endpoints retain the old `fieldValidation` pattern pending a separate ecommerce validation pass.
### Positive Consequences
* Validation always runs before the controller — no risk of continuing on invalid input.
* Rules are co-located with routes, making the contract of each endpoint visible at a glance.
* Reusable rule sets can be composed and shared across routes.
* `fieldValidation` removed from users and articles controllers — no more duplicated logic.
### Negative Consequences
* Products and tags controllers still use the old `fieldValidation` pattern — inconsistency until the ecommerce pass is done.
* Validation rules must be maintained in sync with model constraints (e.g. if a column length changes, the validator must be updated manually).
## Pros and Cons of the Options
### Keep and fix fieldValidation
* Good, because no new dependency.
* Bad, because it is duplicated in every controller that needs it.
* Bad, because it belongs in controllers, not at the boundary where input arrives.
### Joi
* Good, because schema-based validation with rich type coercion.
* Bad, because requires a wrapper middleware to integrate with Express — adds boilerplate not present in the existing codebase.
### express-validator
* Good, because native Express middleware — no wrapper needed.
* Good, because rule sets are plain arrays, easy to compose and test independently.
* Good, because actively maintained with wide adoption in Express projects.
* Bad, because rules are imperative chains rather than declarative schemas, which can be verbose for complex objects.
-61
View File
@@ -1,61 +0,0 @@
# Upgrade Express from v4 to v5 and remove redundant middleware
* Status: accepted
* Date: 2026-04-28
## Context and Problem Statement
The API ran on Express 4.18, with two additional middleware packages (`method-override`, `errorhandler`) and an `asyncHandler` wrapper used across all controllers to forward async errors to Express. Express 5 was released as stable in September 2024 and addresses the async error handling gap natively.
## Decision Drivers
* Express 5 propagates promise rejections in route handlers to error middleware automatically — the `asyncHandler` wrapper is redundant noise.
* `method-override` allows clients to tunnel `PUT`/`DELETE` via `POST` using a `_method` parameter. The API is consumed exclusively by Angular's `HttpClient`, which sends correct HTTP verbs directly. The middleware has never been exercised.
* `errorhandler` provides verbose error pages in development, but a custom `exceptions.handler.js` already handles error formatting for all environments.
* Removing unused middleware reduces the attack surface and the number of dependencies to keep up to date.
## Considered Options
* Stay on Express 4, keep existing middleware
* Upgrade to Express 5, keep `method-override` and `errorhandler` for safety
* Upgrade to Express 5 and remove all three redundant pieces
## Decision Outcome
Chosen option: "upgrade to Express 5 and remove all three redundant pieces", because the migration has zero breaking changes for this codebase (no wildcard routes, no removed APIs in use) and each removed package was demonstrably unused or superseded.
Changes made:
- `express` bumped to `^5` (installed 5.2.1)
- `method-override` removed from `package.json` and `createApp.js`
- `errorhandler` removed from `package.json` and `createApp.js`
- `asyncHandler` wrapper removed from all 8 controllers and its middleware file deleted
- Controller exports changed from `asyncHandler(async (req, res, next) => { ... })` to `async (req, res, next) => { ... }`
### Positive Consequences
* Unhandled async errors now reach the error handler without any wrapper — less boilerplate in every controller.
* Two dependencies removed from the production bundle (`method-override`, `errorhandler`).
* Codebase is on a maintained major version with long-term support.
### Negative Consequences
* Express 5 is a major version bump — future middleware additions must be verified for Express 5 compatibility before adoption.
## Pros and Cons of the Options
### Stay on Express 4
* Good, because no migration effort.
* Bad, because the async error gap requires the `asyncHandler` wrapper indefinitely.
* Bad, because `method-override` and `errorhandler` remain as unused surface area.
### Upgrade to Express 5, keep existing middleware
* Good, because conservative — no behavioural changes beyond async propagation.
* Bad, because `method-override` and `errorhandler` are still dead weight.
### Upgrade to Express 5 and remove all three
* Good, because every removed package is one fewer dependency to audit and update.
* Good, because controller code is simpler without the wrapper.
* Bad, because a future Express 4-only middleware would need to be replaced — acceptable trade-off given the ecosystem has largely moved to v5.
@@ -1,60 +0,0 @@
# Extract user and application routes into a dedicated auth domain
* Status: accepted
* Date: 2026-05-01
## Context and Problem Statement
User authentication/registration routes (`/api/cms/user`) were grouped under the CMS domain, and API key application routes (`/api/skydive/applications`) were grouped under the Skydive domain. Neither resource is domain-specific content: users and applications are identity and access management concerns shared across all domains.
## Decision Drivers
* The domain prefix should reflect what a resource *is*, not which feature first needed it.
* `/api/cms/user` implies users are CMS content; `/api/skydive/applications` implies API keys are skydive data — both are misleading.
* A dedicated `auth` domain makes the security boundary explicit and easier to apply targeted middleware (rate limiting, stricter CORS, etc.) in the future.
## Considered Options
* Keep routes in their current domains
* Move only users out of CMS, leave applications in skydive
* Create a dedicated `auth` domain for both
## Decision Outcome
Chosen option: "dedicated `auth` domain", because both resources are identity/access concerns and grouping them together makes the API surface self-documenting.
Changes made:
- `src/routes/api/auth/` created with `users.routes.js`, `applications.routes.js`, and `index.js`
- `/user` removed from `src/routes/api/cms/index.js`
- `/applications` removed from `src/routes/api/skydive/index.js`
- `/auth` domain registered in `src/routes/api/index.js`
- Frontend `user.service.ts` and `applications.service.ts` updated: `_apiDomain` changed from `/cms` and `/skydive` to `/auth`
Resulting routes: `/api/auth/user/*` and `/api/auth/applications/*`.
### Positive Consequences
* API domain structure matches resource semantics — auth concerns are isolated from content and feature domains.
* A single place to tighten auth-specific middleware (rate limiting, IP allowlists, audit logging) without touching other domains.
### Negative Consequences
* Breaking change on the API surface — any client other than the Angular frontend calling the old paths must be updated.
## Pros and Cons of the Options
### Keep routes in current domains
* Good, because no migration effort.
* Bad, because the domain prefix actively misleads — users are not CMS content, applications are not skydive data.
### Move only users out of CMS
* Good, because smaller change.
* Bad, because applications in skydive remains wrong, and two related resources end up in different places.
### Dedicated auth domain
* Good, because both resources land where their semantics say they belong.
* Good, because the auth boundary is explicit and ready for future hardening.
* Bad, because it is a breaking change requiring a coordinated update of all consumers.
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
testEnvironment: 'node',
setupFiles: ['./tests/jest.env.js'],
testMatch: ['**/tests/api/**/*.test.js'],
testTimeout: 15000,
maxWorkers: 1,
verbose: true,
forceExit: true,
};
+7
View File
@@ -0,0 +1,7 @@
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch((err) => {
next(err);
});
};
module.exports = asyncHandler;
+30
View File
@@ -0,0 +1,30 @@
const { verify } = require("../util/jwt");
const User = require("../models/User");
const ErrorResponse = require("../util/errorResponse");
exports.protect = async (req, res, next) => {
try {
const { headers } = req;
if (!headers.authorization) return next();
const token = headers.authorization.split(" ")[1];
if (!token) throw new SyntaxError("Token missing or malformed");
const userVerified = await verify(token);
if (!userVerified) throw new Error("Invalid Token");
req.loggedUser = await User.findOne({
attributes: { exclude: ["email", "password"] },
where: { email: userVerified.email },
});
if (!req.loggedUser) next(new NotFoundError("User"));
headers.email = userVerified.email;
req.loggedUser.dataValues.token = token;
next();
} catch (error) {
next(error);
}
};
+22
View File
@@ -0,0 +1,22 @@
const ErrorResponse = require("../util/errorResponse");
//REQUESTED PAGE IS NOT FOUND
module.exports.notFound = (req, res, next) => {
const error = new ErrorResponse(`Not Found - ${req.originalUrl}`);
res.status(404);
next(error);
};
module.exports.errorHandler = (err, req, res, next) => {
// Log to console for dev
console.log(err.message.red);
console.log(err.stack.red);
const statusCode = err.statusCode ? err.statusCode : 500;
res.status(statusCode).json({
errors: {
body: [err.message],
},
});
};
+34
View File
@@ -0,0 +1,34 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../util/database");
const slugify = require("slugify");
const Article = sequelize.define("Article", {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false },
slug: { type: DataTypes.STRING, allowNull: false },
title: { type: DataTypes.STRING, allowNull: false },
description: { type: DataTypes.TEXT },
body: { type: DataTypes.TEXT, allowNull: false }
}, {
sequelize,
timestamps: true,
modelName: 'Article',
tableName: 'article',
createdAt: true,
updatedAt: true
});
Article.beforeValidate((article) => {
article.slug = slugify(article.title, { lower: true });
});
Article.prototype.toJSONFor = function () {
return {
id: this.id,
slug: this.slug,
title: this.title,
description: this.description,
body: this.body
};
};
module.exports = Article;
+23
View File
@@ -0,0 +1,23 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../util/database");
const Comment = sequelize.define("Comment", {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false },
body: { type: DataTypes.TEXT, allowNull: false }
}, {
sequelize,
timestamps: true,
modelName: 'Comment',
tableName: 'comment',
createdAt: true,
updatedAt: true
});
Comment.prototype.toJSONFor = function () {
return {
id: this.id,
body: this.body
};
};
module.exports = Comment;
+21
View File
@@ -0,0 +1,21 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../util/database");
const Tag = sequelize.define("Tag", {
name: { type: DataTypes.STRING, primaryKey: true, allowNull: false }
}, {
sequelize,
timestamps: true,
modelName: 'Tag',
tableName: 'tag',
createdAt: true,
updatedAt: true
});
Tag.prototype.toJSONFor = function () {
return {
name: this.name
};
};
module.exports = Tag;
+52
View File
@@ -0,0 +1,52 @@
const { DataTypes, Model } = require("sequelize");
const sequelize = require("../util/database");
const bcrypt = require("bcryptjs");
const User = sequelize.define("User", {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false },
email: { type: DataTypes.STRING, allowNull: false, unique: true },
username: { type: DataTypes.STRING, allowNull: false, unique: true },
firstname: { type: DataTypes.STRING },
lastname: { type: DataTypes.STRING },
bio: { type: DataTypes.TEXT, allowNull: true },
image: { type: DataTypes.TEXT, allowNull: true },
role: { type: DataTypes.STRING },
password: { type: DataTypes.STRING, allowNull: false }
}, {
sequelize,
timestamps: true,
modelName: 'User',
tableName: 'user',
createdAt: true,
updatedAt: true
});
Model.prototype.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
const DEFAULT_SALT_ROUNDS = 10;
User.addHook("beforeCreate", async (user) => {
const encryptedPassword = await bcrypt.hash(
user.password,
DEFAULT_SALT_ROUNDS
);
user.password = encryptedPassword;
});
User.prototype.toJSONFor = function () {
return {
id: this.id,
email: this.email,
username: this.username,
firstname: this.firstname,
lastname: this.lastname,
bio: this.bio,
image: this.image,
role: this.role
};
};
module.exports = User;
+4
View File
@@ -0,0 +1,4 @@
exports.User = require('./User');
exports.Article = require('./Article');
exports.Comment = require('./Comment');
exports.Tag = require('./Tag');
+48
View File
@@ -0,0 +1,48 @@
const slugify = require("slugify");
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Article extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
/* define association here */
}
}
Article.init({
articleId: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false },
slug: { type: DataTypes.STRING, allowNull: false },
title: { type: DataTypes.STRING, allowNull: false },
description: { type: DataTypes.TEXT },
body: { type: DataTypes.TEXT, allowNull: false }
}, {
sequelize,
timestamps: false,
modelName: 'Article',
tableName: 'article',
createdAt: true,
updatedAt: true
});
Article.beforeValidate((article) => {
article.slug = slugify(article.title, { lower: true });
});
Article.prototype.toJSONFor = function () {
return {
articleId: this.articleId,
slug: this.slug,
title: this.title,
description: this.description,
body: this.body
};
};
return Article;
};
+35
View File
@@ -0,0 +1,35 @@
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Comment extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
/* define association here */
}
}
Comment.init({
commentId: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false },
body: { type: DataTypes.TEXT, allowNull: false }
}, {
sequelize,
timestamps: false,
modelName: 'Comment',
tableName: 'comment',
createdAt: true,
updatedAt: true
});
Comment.prototype.toJSONFor = function () {
return {
commentId: this.commentId,
body: this.body
};
};
return Comment;
};
+33
View File
@@ -0,0 +1,33 @@
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Tag extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
/* define association here */
}
}
Tag.init({
name: { type: DataTypes.STRING, primaryKey: true, allowNull: false }
}, {
sequelize,
timestamps: false,
modelName: 'Tag',
tableName: 'tag',
createdAt: true,
updatedAt: true
});
Tag.prototype.toJSONFor = function () {
return {
name: this.name
};
};
return Tag;
};
+65
View File
@@ -0,0 +1,65 @@
'use strict';
const bcrypt = require("bcryptjs");
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class User extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
/* define association here */
}
}
User.init({
userId: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
email: {type: DataTypes.STRING, allowNull: false, unique: true},
username: {type: DataTypes.STRING, allowNull: false, unique: true},
firstname: {type: DataTypes.STRING},
lastname: {type: DataTypes.STRING},
bio: {type: DataTypes.TEXT, allowNull: true},
image: {type: DataTypes.TEXT, allowNull: true},
role: {type: DataTypes.STRING},
password: {type: DataTypes.STRING, allowNull: false}
}, {
sequelize,
timestamps: false,
modelName: 'User',
tableName: 'user',
createdAt: true,
updatedAt: true
});
User.prototype.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
const DEFAULT_SALT_ROUNDS = 10;
User.addHook("beforeCreate", async (user) => {
const encryptedPassword = await bcrypt.hash(
user.password,
DEFAULT_SALT_ROUNDS
);
user.password = encryptedPassword;
});
User.prototype.toJSONFor = function () {
return {
userId: this.userId,
email: this.email,
username: this.username,
firstname: this.firstname,
lastname: this.lastname,
bio: this.bio,
image: this.image,
role: this.role
};
};
return User;
};
+50
View File
@@ -0,0 +1,50 @@
const fs = require('fs'),
path = require('path'),
basename = path.basename(__filename),
chalk = require('chalk'),
utils = require('../../routes/utils');
const { Sequelize } = require('sequelize');
const db = {};
const Op = Sequelize.Op;
const sequelize = new Sequelize(
process.env.MYSQL_DBNAME,
process.env.MYSQL_DBUSER,
process.env.MYSQL_DBPASS,
{
host: process.env.MYSQL_DBHOST,
port: process.env.MYSQL_DBPORT,
dialect: process.env.MYSQL_DBTYPE,
$like: Op.like,
$not: Op.not
}
);
sequelize.authenticate().then(() => {
if (process.env.DEBUG) {
console.log(`${utils.getDateTimeISOString()}`, chalk.green('MySQL connection has been established successfully'));
}
}).catch((error) => {
console.log(`${utils.getDateTimeISOString()}`, chalk.red('Unable to connect to the MySQL database'));
console.error(error);
});
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
module.exports = db;
+18 -2
View File
@@ -1,5 +1,21 @@
{
"env": {
"APP_ENV": "local"
"env" : {
"APP_ENV": "local",
"NODE_ENV" : "development",
"DEBUG": false,
"SERVER_PORT": "3201",
"JWT_SECRET": "$2b$10$aEsAUG7Dy8dcTORi1vaQle",
"SESSION_LIFETIME": "3600",
"MONGODB_URI": "mongodb://localhost:27017/adastradb",
"SENDGRID_API_KEY": "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",
"SENDGRID_FROM_MAIL": "contact@rampeur.fr",
"SENDGRID_TO_MAIL": "rampeur@gmail.com",
"AUTHORIZATION_PREFIX": "Api-Key",
"COUCHDB_URL": "http://couchdb.unespace.com",
"COUCHDB_DATABASE": "adastradb",
"COUCHDB_DESIGN": "adastra_app",
"COUCHDB_CREDENTIAL": "cmFtcGV1cjpES3hwMjRQUw==",
"SKYDIVERID_API_URL": "https://skydiver.id/api",
"SKYDIVERID_API_TOKEN": "xmqzgnGYMD.OJgFw1pCXCpJFGmZfSjmWK8lrpqAQNnu"
}
}
+834 -8386
View File
File diff suppressed because it is too large Load Diff
+26 -55
View File
@@ -2,68 +2,39 @@
"name": "adastra-api",
"version": "1.0.0",
"description": "Ad Astra API",
"main": "app.js",
"author": {
"name": "Julien Gautier",
"email": "jgautier.webdev@gmail.com"
},
"private": true,
"license": "UNLICENSED",
"author": "Solide Apps <jgautier.webdev@gmail.com>",
"contributors": [
"Julien Gautier <jgautier.webdev@gmail.com>"
],
"main": "server.js",
"scripts": {
"start": "APP_ENV=production node ./app.js",
"dev": "APP_ENV=development node ./app.js",
"local": "APP_ENV=local nodemon ./app.js",
"test": "jest",
"test:postman": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json",
"stop": "lsof -ti :3200 | xargs kill",
"mongo:start": "docker run --name hu-mongo -p 27017:27017 mongo & sleep 5",
"mongo:stop": "docker stop hu-mongo && docker rm hu-mongo",
"create:user": "node scripts/create-user.js",
"setup:api": "npx sequelize-cli db:migrate && npx sequelize-cli db:seed --seed 20251126-add-roles.js && npm run create:user && npx sequelize-cli db:seed --seed 20251127-add-articles.js"
"start": "APP_ENV=production node ./server.js",
"dev": "APP_ENV=development node ./server.js",
"local": "APP_ENV=local nodemon ./server.js",
"test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json"
},
"engines": {
"node": "^16.20.2 || ^18.19.1 || ^20.11.1"
},
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"bcrypt": "^5.1.1",
"cors": "^2.8.6",
"dotenv": "^16.6.1",
"ejs": "^3.1.10",
"express": "^5",
"express-jwt": "^8.5.1",
"express-rate-limit": "^8.4.1",
"express-session": "^1.19.0",
"express-validator": "^7.3.2",
"helmet": "^8.1.0",
"js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.3",
"methods": "1.1.2",
"mongoose": "^6.12.6",
"mongoose-unique-validator": "^3.1.0",
"morgan": "^1.10.1",
"mysql2": "^3.22.3",
"node-fetch": "^3.3.2",
"passport": "^0.6.0",
"passport-headerapikey": "^1.2.2",
"passport-local": "^1.0.0",
"request": "^2.88.2",
"sequelize": "^6.37.8",
"sequelize-auto": "^0.8.8",
"slug": "^8.2.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"underscore": "^1.13.8",
"uuid": "^9.0.1"
"@sendgrid/mail": "^8.1.5",
"bcryptjs": "^2.4.3",
"colors": "^1.4.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"mysql2": "^3.14.3",
"nodemon": "^3.1.10",
"sequelize": "^6.26.0",
"slugify": "^1.6.5"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"eslint": "^9.39.4",
"globals": "^15.15.0",
"jest": "^30.3.0",
"newman": "^5.3.2",
"nodemon": "^3.1.14",
"sequelize-cli": "^6.6.5",
"supertest": "^7.2.2"
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
"prettier": "^2.8.0",
"sequelize-cli": "^6.6.3"
}
}
View File
-3563
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
const express = require("express");
const router = express.Router();
const {
getArticle,
getArticles,
createArticle,
articlesFeed,
addFavoriteArticle,
deleteFavoriteArticle,
deleteArticle,
updateArticle,
} = require("../../controllers/articles");
const { protect } = require("../../middlewares/auth");
router
.route("/articles")
.get(protect, getArticles)
.post(protect, createArticle);
router.route("/articles/feed").get(protect, articlesFeed);
router
.route("/articles/:slug")
.get(protect, getArticle)
.put(protect, updateArticle)
.delete(protect, deleteArticle);
router
.route("/articles/:slug/favorite")
.post(protect, addFavoriteArticle)
.delete(protect, deleteFavoriteArticle);
module.exports = router;
+23
View File
@@ -0,0 +1,23 @@
const express = require("express");
const router = express.Router();
const {
getComment,
getComments,
createComment,
deleteComment,
} = require("../../controllers/comments");
const { protect } = require("../../middlewares/auth");
router
.route("/articles/:slug/comments")
.get(protect, getComments)
.post(protect, createComment);
router
.route("/articles/:slug/comments/:id")
.get(protect, getComment)
.delete(protect, deleteComment);
module.exports = router;
+22
View File
@@ -0,0 +1,22 @@
var router = require('express').Router();
router.use('/', require('./profiles'));
router.use('/articles', require('./articles'));
router.use('/comments', require('./comments'));
router.use('/tags', require('./tags'));
router.use('/profiles', require('./profiles'));
router.use('/users', require('./users'));
router.use(function (err, req, res, next) {
if (err.name === 'ValidationError') {
return res.status(422).json({
errors: Object.keys(err.errors).reduce(function (errors, key) {
errors[key] = err.errors[key].message;
return errors;
}, {})
});
}
return next(err);
});
module.exports = router;
+16
View File
@@ -0,0 +1,16 @@
const express = require("express");
const router = express.Router();
const {
getProfile,
followUser,
unfollowUser,
} = require("../../controllers/profiles");
const { protect } = require("../../middlewares/auth");
router.route("/profiles/:username").get(protect, getProfile);
router
.route("/profiles/:username/follow")
.post(protect, followUser)
.delete(protect, unfollowUser);
module.exports = router;
+8
View File
@@ -0,0 +1,8 @@
const express = require("express");
const router = express.Router();
const { getTags } = require("../../controllers/tags");
router.route("/tags/").get(getTags);
module.exports = router;
+16
View File
@@ -0,0 +1,16 @@
const express = require("express");
const router = express.Router();
const {
createUser,
loginUser,
getCurrentUser,
updateUser,
} = require("../../controllers/users");
const { protect } = require("../../middlewares/auth");
router.post("/users", createUser);
router.post("/users/login", loginUser);
router.route("/user").get(protect, getCurrentUser).put(protect, updateUser);
module.exports = router;
+4
View File
@@ -0,0 +1,4 @@
var router = require('express').Router();
router.use('/api', require('./api'));
module.exports = router;
+10 -13
View File
@@ -1,26 +1,23 @@
class DateUtilities {
static getDateString() {
const utils = {
getDateString: () => {
let today = new Date();
let dateNow = today.toLocaleDateString();
return `${dateNow}`;
}
static getTimeString() {
},
getTimeString: () => {
let today = new Date();
let timeNow = today.toLocaleTimeString();
return `${timeNow}`;
}
static getDateTimeString(delimiter = true) {
},
getDateTimeString: (delimiter = true) => {
let today = new Date();
let dateString = `${today.toLocaleDateString()} ${today.toLocaleTimeString()}`;
if (delimiter) {
dateString = `[${dateString}]`;
}
return dateString;
}
static getDateTimeISOString(delimiter = true) {
},
getDateTimeISOString: (delimiter = true) => {
let today = new Date();
let dateString = today.toISOString();
if (delimiter) {
@@ -28,6 +25,6 @@ class DateUtilities {
}
return dateString;
}
}
};
module.exports = DateUtilities;
module.exports = utils;
-177
View File
@@ -1,177 +0,0 @@
#!/usr/bin/env node
/**
* Script pour créer un utilisateur Admin interactivement
* Usage: npm run create:user
*/
const readline = require('readline');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
// Load environment
const appEnv = process.env.APP_ENV || 'development';
require('dotenv').config({ path: `config/env/.env.${appEnv}` });
const sequelize = require('../src/database/config/sequelize');
// Create readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Helper function for prompts
const question = (prompt) => {
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
resolve(answer);
});
});
};
// Helper function for hidden password input
const questionHidden = (prompt) => {
return new Promise((resolve) => {
process.stdout.write(prompt);
// Suppress readline's own software echo so only '*' appears
const originalWrite = rl._writeToOutput;
rl._writeToOutput = () => {};
process.stdin.setRawMode(true);
process.stdin.resume();
let password = '';
const dataListener = (char) => {
char = char.toString();
switch (char) {
case '\n':
case '\r':
case '':
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdout.write('\n');
process.stdin.removeListener('data', dataListener);
rl._writeToOutput = originalWrite;
resolve(password);
break;
case '':
process.exit();
break;
case '':
case '\b':
password = password.slice(0, -1);
process.stdout.write('\b \b');
break;
default:
password += char;
process.stdout.write('*');
}
};
process.stdin.on('data', dataListener);
});
};
// Hash password
const hashPassword = (password) => {
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
return { salt, hash };
};
// Main function
const createUser = async () => {
try {
console.log('\n🔐 Création d\'un utilisateur Admin\n');
console.log('Veuillez fournir les informations suivantes:\n');
// Get user input
const email = await question('Email: ');
const username = await question('Nom d\'utilisateur: ');
const firstname = await question('Prénom (optionnel): ');
const lastname = await question('Nom (optionnel): ');
const phone = await question('Téléphone (optionnel): ');
const password = await questionHidden('Mot de passe: ');
const passwordConfirm = await questionHidden('Confirmer le mot de passe: ');
// Validation
if (!email || !username || !password) {
console.error('\n❌ Email, nom d\'utilisateur et mot de passe sont obligatoires\n');
rl.close();
process.exit(1);
}
if (password !== passwordConfirm) {
console.error('\n❌ Les mots de passe ne correspondent pas\n');
rl.close();
process.exit(1);
}
if (password.length < 6) {
console.error('\n❌ Le mot de passe doit contenir au moins 6 caractères\n');
rl.close();
process.exit(1);
}
// Connect to database and load models
console.log('\n📡 Connexion à la base de données...');
await sequelize.authenticate();
console.log('✅ Connexion établie\n');
const DB = require('../src/database/mysql');
require('../src/database/relationships')();
// Hash password with same algorithm as UserService
const { salt, hash } = hashPassword(password);
// Create user
console.log('👤 Création de l\'utilisateur...');
const user = await DB.User.create({
id: uuidv4(),
email: email.toLowerCase(),
username,
firstname: firstname || null,
lastname: lastname || null,
phone: phone || null,
image: null,
role: 'Admin',
salt,
hash
});
// Find and assign Admin role
const adminRole = await DB.Role.findOne({ where: { name: 'Admin' } });
if (adminRole) {
await user.addRoleIdRole(adminRole);
console.log('✅ Rôle Admin assigné\n');
} else {
console.log('⚠️ Rôle Admin non trouvé, création en tant qu\'utilisateur uniquement\n');
}
// Success message
console.log('✅ Utilisateur créé avec succès!\n');
console.log('Détails:');
console.log(` 📧 Email: ${user.email}`);
console.log(` 👤 Nom d'utilisateur: ${user.username}`);
console.log(` 🔐 Rôle: ${user.role}`);
console.log('');
rl.close();
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('\n❌ Erreur lors de la création de l\'utilisateur:\n');
console.error(error.message);
console.error(error.stack);
console.log('');
rl.close();
process.exit(1);
}
};
// Run script
createUser();
-370
View File
@@ -1,370 +0,0 @@
#!/usr/bin/env node
/**
* Cleanup & Testing Script: Finalize Domain-Based API Migration
*
* This script provides:
* 1. Optional cleanup of old v1/v2/v3 directories
* 2. Validation of new domain structure
* 3. Quick endpoint testing
* 4. Summary report
*
* Usage: node scripts/finalize-migration.js [--cleanup] [--test] [--full]
*/
const fs = require('fs');
const path = require('path');
const http = require('http');
const { execSync } = require('child_process');
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
};
const log = {
info: (msg) => console.log(`${colors.cyan}${colors.reset} ${msg}`),
success: (msg) => console.log(`${colors.green}${colors.reset} ${msg}`),
warn: (msg) => console.log(`${colors.yellow}${colors.reset} ${msg}`),
error: (msg) => console.log(`${colors.red}${colors.reset} ${msg}`),
section: (msg) => console.log(`\n${colors.blue}━━━ ${msg} ━━━${colors.reset}\n`),
title: (msg) => console.log(`\n${colors.magenta}╔════════════════════════════════════════╗
${msg.padEnd(38)}
╚════════════════════════════════════════╝${colors.reset}\n`),
};
const args = process.argv.slice(2);
const shouldCleanup = args.includes('--cleanup');
const shouldTest = args.includes('--test');
const fullMode = args.includes('--full');
const projectRoot = path.resolve(__dirname, '..');
const routesDir = path.join(projectRoot, 'src/routes/api');
/**
* Phase 1: Validate new structure exists
*/
function validateNewStructure() {
log.section('Phase 1: Validating new domain structure');
const domains = ['skydive', 'cms', 'herowars'];
let allValid = true;
domains.forEach((domain) => {
const domainPath = path.join(routesDir, domain);
const indexPath = path.join(domainPath, 'index.js');
if (fs.existsSync(domainPath) && fs.existsSync(indexPath)) {
const fileCount = fs.readdirSync(domainPath).length - 1; // Exclude index.js
log.success(`${domain}/: ✓ (${fileCount} route files)`);
} else {
log.error(`${domain}/: ✗ Missing directory or index.js`);
allValid = false;
}
});
return allValid;
}
/**
* Phase 2: List old directories
*/
function listOldDirectories() {
log.section('Phase 2: Old v1/v2/v3 directories status');
const oldDirs = ['v1', 'v2', 'v3'];
let hasOldDirs = false;
oldDirs.forEach((dir) => {
const fullPath = path.join(routesDir, dir);
if (fs.existsSync(fullPath)) {
const fileCount = fs.readdirSync(fullPath).length;
log.warn(`${dir}/: Still exists (${fileCount} files)`);
hasOldDirs = true;
} else {
log.success(`${dir}/: Already removed ✓`);
}
});
return hasOldDirs;
}
/**
* Phase 3: Cleanup old directories
*/
function cleanupOldDirectories() {
log.section('Phase 3: Cleaning up old directories');
const oldDirs = ['v1', 'v2', 'v3'];
oldDirs.forEach((dir) => {
const fullPath = path.join(routesDir, dir);
if (fs.existsSync(fullPath)) {
try {
// Recursively delete directory
const removeDir = (dirPath) => {
if (fs.existsSync(dirPath)) {
fs.readdirSync(dirPath).forEach((f) => {
const filePath = path.join(dirPath, f);
if (fs.lstatSync(filePath).isDirectory()) {
removeDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(dirPath);
}
};
removeDir(fullPath);
log.success(`Removed: ${dir}/`);
} catch (err) {
log.error(`Failed to remove ${dir}/: ${err.message}`);
}
}
});
// Create cleanup commit
try {
execSync('git add -A', { cwd: projectRoot, stdio: 'pipe' });
execSync('git commit -m "chore: remove old v1/v2/v3 route directories after migration"', {
cwd: projectRoot,
stdio: 'pipe',
});
log.success('Created git commit for cleanup');
} catch (err) {
log.warn('Could not auto-commit cleanup');
}
}
/**
* Phase 4: Test endpoints
*/
function testEndpoints() {
log.section('Phase 4: Testing endpoints');
const endpoints = [
{ method: 'GET', path: '/api-docs', description: 'Swagger UI' },
{ method: 'GET', path: '/api/skydive/jumps', description: 'Skydive jumps' },
{ method: 'GET', path: '/api/cms/articles', description: 'CMS articles' },
{ method: 'GET', path: '/api/herowars/clans', description: 'Hero Wars clans' },
];
const testEndpoint = (method, urlPath, description) => {
return new Promise((resolve) => {
const req = http.request(
{
hostname: 'localhost',
port: 3200,
path: urlPath,
method: method,
timeout: 5000,
},
(res) => {
if (res.statusCode === 200 || res.statusCode === 401) {
// 401 might happen due to auth, but endpoint exists
log.success(`${description} (${res.statusCode})`);
resolve(true);
} else if (res.statusCode === 404) {
log.error(`${description} (404 - not found)`);
resolve(false);
} else {
resolve(true); // Consider other status codes as working
}
}
);
req.on('error', (err) => {
log.error(`${description} (Connection error)`);
resolve(false);
});
req.on('timeout', () => {
req.destroy();
log.error(`${description} (Timeout)`);
resolve(false);
});
req.end();
});
};
log.info('Pinging endpoints...\n');
// Note: This would require the server to be running
log.warn('Endpoint testing requires running server (npm run local)');
log.info('Manual test commands:');
endpoints.forEach(({ method, path, description }) => {
console.log(` curl http://localhost:3200${path} # ${description}`);
});
}
/**
* Phase 5: Generate final summary
*/
function generateFinalSummary() {
log.section('Phase 5: Final Summary');
const summary = `
╔════════════════════════════════════════════════════════════════════════════╗
║ MIGRATION SUCCESSFULLY COMPLETED ║
╚════════════════════════════════════════════════════════════════════════════╝
NEW API STRUCTURE:
├── /api/skydive/* ← Skydiving features (jumps, aeronefs, dropzones, etc.)
├── /api/cms/* ← Content management (articles, tags, comments, etc.)
└── /api/herowars/* ← Game features (clans, members, raids, etc.)
WHAT WAS CHANGED:
✓ Routes directory: v1/v2/v3 → domain-based structure
✓ Route definitions: Updated all index.js files
✓ Swagger YAML: Updated path prefixes to new domains
✓ Postman collection: Updated endpoint URLs
FILES MIGRATED:
✓ Skydive domain (8 route files):
- aeronefs.js, applications.js, canopies.js, dropzones.js
- jumps.js, pages.js, profiles.js, qcm.js
✓ CMS domain (8 route files):
- articles.routes.js, comments.routes.js, product.routes.js
- products.routes.js, profiles.routes.js, tags.routes.js, user.routes.js
✓ Hero Wars domain (3 route files):
- clans.routes.js, members.routes.js, herowars.routes.js
TESTING INSTRUCTIONS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Start the API server:
npm run local
2. In another terminal, verify Swagger is updated:
curl http://localhost:3200/api-docs
3. Test a sample endpoint:
curl http://localhost:3200/api/skydive/jumps
curl http://localhost:3200/api/cms/articles
curl http://localhost:3200/api/herowars/clans
FRONTEND UPDATES REQUIRED:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
In your Angular frontend (adastra_app):
1. Update environment files:
src/environments/environment.ts
src/environments/environment.development.ts
src/environments/environment.local.ts
Replace:
- apiUrl: '/api/v1' → apiUrl: '/api/skydive'
- cmUrl: '/api/v2' → cmUrl: '/api/cms'
- hwUrl: '/api/v3' → hwUrl: '/api/herowars'
2. Update API service calls:
// Before:
http.get('/api/v1/jumps')
http.get('/api/v2/articles')
http.get('/api/v3/clans')
// After:
http.get('/api/skydive/jumps')
http.get('/api/cms/articles')
http.get('/api/herowars/clans')
3. Search & replace in your services:
sed -i 's|/api/v1/|/api/skydive/|g' src/**/*.ts
sed -i 's|/api/v2/|/api/cms/|g' src/**/*.ts
sed -i 's|/api/v3/|/api/herowars/|g' src/**/*.ts
GIT OPERATIONS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
View migration commits:
git log --oneline -5
If needed, view detailed changes:
git show HEAD
Push to remote:
git push origin develop
ROLLBACK (if issues):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
If you need to rollback:
git reset --hard HEAD~2
(Adjust the number of commits based on history)
ADDITIONAL RESOURCES:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Documentation files created:
- MIGRATION_REPORT.md: Detailed migration information
- MIGRATION_SWAGGER_TODO.md: Swagger update notes
- POST_MIGRATION_CHECKLIST.md: Verification checklist
Scripts created:
- scripts/migrate-to-domain-structure.js: Main migration script
- scripts/update-references-post-migration.js: Post-migration updates
- scripts/finalize-migration.js: Cleanup and testing
═════════════════════════════════════════════════════════════════════════════
✓ Migration is complete! Follow the testing instructions above.
═════════════════════════════════════════════════════════════════════════════
`;
const summaryPath = path.join(projectRoot, 'MIGRATION_COMPLETE_SUMMARY.md');
fs.writeFileSync(summaryPath, summary);
console.log(summary);
log.success(`Full summary saved to: MIGRATION_COMPLETE_SUMMARY.md`);
}
/**
* Main execution
*/
function main() {
log.title('Domain-Based Migration Finalization');
try {
const isValid = validateNewStructure();
const hasOldDirs = listOldDirectories();
if (!isValid) {
log.error('New structure validation failed!');
process.exit(1);
}
if (shouldCleanup && hasOldDirs) {
const confirm = fullMode ? true : false;
if (confirm || fullMode) {
log.warn('\nRemoving old v1/v2/v3 directories...');
cleanupOldDirectories();
} else {
log.info('\nTo remove old directories, run:');
log.info(' rm -rf src/routes/api/v1 src/routes/api/v2 src/routes/api/v3');
}
}
if (shouldTest || fullMode) {
testEndpoints();
}
generateFinalSummary();
log.success(`\n✓ Finalization complete!\n`);
} catch (error) {
log.error(`Finalization failed: ${error.message}`);
process.exit(1);
}
}
main();
-481
View File
@@ -1,481 +0,0 @@
#!/usr/bin/env node
/**
* Migration Script: Restructure API from v1/v2/v3 to Domain-Based (skydive/cms/herowars)
*
* This script automates the transformation:
* - /api/v1/* → /api/skydive/* (Skydive domain)
* - /api/v2/* → /api/cms/* (CMS domain)
* - /api/v3/* → /api/herowars/* (Hero Wars domain)
*
* Usage: node scripts/migrate-to-domain-structure.js [--dry-run] [--skip-git]
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Colors for console output
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
blue: '\x1b[34m',
};
const log = {
info: (msg) => console.log(`${colors.cyan}${colors.reset} ${msg}`),
success: (msg) => console.log(`${colors.green}${colors.reset} ${msg}`),
warn: (msg) => console.log(`${colors.yellow}${colors.reset} ${msg}`),
error: (msg) => console.log(`${colors.red}${colors.reset} ${msg}`),
section: (msg) => console.log(`\n${colors.blue}━━━ ${msg} ━━━${colors.reset}\n`),
};
// Parse command line arguments
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const skipGit = args.includes('--skip-git');
const projectRoot = path.resolve(__dirname, '..');
const routesDir = path.join(projectRoot, 'src/routes/api');
// Domain mapping
const domainMap = {
v1: {
domain: 'skydive',
routes: {
'/applications': 'applications',
'/aeronefs': 'aeronefs',
'/canopies': 'canopies',
'/dropzones': 'dropzones',
'/jumps': 'jumps',
'/pages': 'pages',
'/profiles': 'profiles',
'/qcm': 'qcm',
},
},
v2: {
domain: 'cms',
routes: {
'/user': 'user.routes',
'/articles': 'articles.routes',
'/comments': 'comments.routes',
'/product': 'product.routes',
'/products': 'products.routes',
'/profiles': 'profiles.routes',
'/tags': 'tags.routes',
},
// herowars will be moved to v3
},
v3: {
domain: 'herowars',
routes: {
'/clans': 'clans.routes',
'/members': 'members.routes',
'/herowars': 'herowars.routes', // to be moved from v2
},
},
};
/**
* Phase 1: Create new domain directories
*/
function createDomainDirectories() {
log.section('Phase 1: Creating domain directories');
Object.values(domainMap).forEach(({ domain }) => {
const domainPath = path.join(routesDir, domain);
if (!fs.existsSync(domainPath)) {
if (!dryRun) {
fs.mkdirSync(domainPath, { recursive: true });
}
log.success(`Created directory: ${domain}/`);
} else {
log.warn(`Directory already exists: ${domain}/`);
}
});
}
/**
* Phase 2: Copy/move route files to new domain directories
*/
function migrateRouteFiles() {
log.section('Phase 2: Migrating route files');
// Handle v1 → skydive and v2 → cms
['v1', 'v2', 'v3'].forEach((version) => {
const { domain, routes } = domainMap[version];
const oldVersionDir = path.join(routesDir, version);
const newDomainDir = path.join(routesDir, domain);
log.info(`\nMigrating ${version}${domain}:`);
Object.entries(routes).forEach(([routePath, fileName]) => {
const extensions = ['', '.js'];
let fileFound = false;
for (const ext of extensions) {
const oldFilePath = path.join(oldVersionDir, fileName + ext);
if (fs.existsSync(oldFilePath)) {
const newFilePath = path.join(newDomainDir, path.basename(oldFilePath));
if (!dryRun) {
const content = fs.readFileSync(oldFilePath, 'utf8');
fs.writeFileSync(newFilePath, content);
}
log.success(` Copied: ${fileName + ext}${domain}/`);
fileFound = true;
break;
}
}
if (!fileFound) {
log.warn(` File not found: ${fileName}(.js)?`);
}
});
});
// Handle herowars.routes.js move from v2 to v3
const herowarsFile = path.join(routesDir, 'v2', 'herowars.routes.js');
if (fs.existsSync(herowarsFile)) {
const newPath = path.join(routesDir, 'herowars', 'herowars.routes.js');
if (!dryRun) {
const content = fs.readFileSync(herowarsFile, 'utf8');
fs.writeFileSync(newPath, content);
}
log.success(` Moved herowars.routes.js from v2 → herowars/`);
}
}
/**
* Phase 3: Create new index.js files for each domain
*/
function createIndexFiles() {
log.section('Phase 3: Creating index files for domains');
Object.entries(domainMap).forEach(([version, { domain, routes }]) => {
const indexPath = path.join(routesDir, domain, 'index.js');
// Filter out herowars from v2 routes if it's the cms domain
const filteredRoutes =
domain === 'cms'
? Object.entries(routes).filter(([, fileName]) => !fileName.includes('herowars'))
: Object.entries(routes);
const routeImports = filteredRoutes
.map(([routePath, fileName]) => `routes.use('${routePath}', require('./${fileName}'));`)
.join('\n');
const indexContent = `var routes = require('express').Router();
${routeImports}
routes.use(function (err, req, res, next) {
if (err.name === 'ValidationError') {
return res.status(422).json({
errors: Object.keys(err.errors).reduce(function (errors, key) {
errors[key] = err.errors[key].message;
return errors;
}, {})
});
}
return next(err);
});
module.exports = routes;
`;
if (!dryRun) {
fs.writeFileSync(indexPath, indexContent);
}
log.success(`Created: ${domain}/index.js`);
});
}
/**
* Phase 4: Update src/routes/api/index.js
*/
function updateApiIndex() {
log.section('Phase 4: Updating src/routes/api/index.js');
const apiIndexPath = path.join(routesDir, 'index.js');
const newContent = `var routes = require('express').Router();
routes.use('/skydive', require('./skydive'));
routes.use('/cms', require('./cms'));
routes.use('/herowars', require('./herowars'));
module.exports = routes;
`;
if (!dryRun) {
fs.writeFileSync(apiIndexPath, newContent);
}
log.success(`Updated routes/api/index.js`);
}
/**
* Phase 5: Update app.js for Swagger documentation
*/
function updateSwaggerPaths() {
log.section('Phase 5: Creating Swagger path update instructions');
const swaggerUpdateInstructions = `
# Swagger Path Updates Required
The following Swagger YAML files need path updates:
## skydive.yaml
- Change: /api/v1/ → /api/skydive/
- Affected routes: /jumps, /aeronefs, /dropzones, /canopies, /pages, /qcm, /applications
## cms.yaml
- Change: /api/v2/ → /api/cms/
- Affected routes: /articles, /comments, /products, /tags, /profiles, /user
## herowars.yaml
- Change: /api/v3/ → /api/herowars/
- Affected routes: /clans, /members
- Note: Add herowars endpoint from former /api/v2/herowars
Update in: src/config/swagger.js if paths are hardcoded.
`;
if (!dryRun) {
fs.writeFileSync(path.join(projectRoot, 'MIGRATION_SWAGGER_TODO.md'), swaggerUpdateInstructions);
}
log.success(`Created MIGRATION_SWAGGER_TODO.md`);
}
/**
* Phase 6: Create Swagger YAML files for new structure
*/
function createSwaggerYamlFiles() {
log.section('Phase 6: Updating Swagger YAML structure');
const swaggerDir = path.join(projectRoot, 'src/swagger');
// Read existing YAML files and update paths
const updateSwaggerFile = (oldName, newName, newBasePath) => {
const oldPath = path.join(swaggerDir, oldName);
if (fs.existsSync(oldPath)) {
let content = fs.readFileSync(oldPath, 'utf8');
// Update paths in YAML
content = content.replace(/\/api\/v\d+/g, `/api/${newBasePath}`);
const newPath = path.join(swaggerDir, newName);
if (!dryRun) {
fs.writeFileSync(newPath, content);
}
log.success(`Created/Updated: ${newName}`);
}
};
// Note: This depends on what swagger files exist
log.warn(`Manual Swagger file updates may be needed`);
}
/**
* Phase 7: Git operations
*/
function gitOperations() {
log.section('Phase 7: Git operations');
if (skipGit) {
log.warn('Skipping git operations (--skip-git flag)');
return;
}
try {
if (!dryRun) {
// Stage new files
execSync('git add -A', { cwd: projectRoot, stdio: 'pipe' });
log.success(`Staged changes with git`);
// Create commit
const commitMessage = 'refactor: restructure API routes from v1/v2/v3 to domain-based (skydive/cms/herowars)';
execSync(`git commit -m "${commitMessage}"`, { cwd: projectRoot, stdio: 'pipe' });
log.success(`Created git commit`);
} else {
log.info(`Would stage and commit changes to git`);
}
} catch (err) {
log.error(`Git operation failed: ${err.message}`);
}
}
/**
* Phase 8: Post-migration validation
*/
function validateMigration() {
log.section('Phase 8: Validation');
Object.entries(domainMap).forEach(([version, { domain, routes }]) => {
const domainPath = path.join(routesDir, domain);
const indexPath = path.join(domainPath, 'index.js');
// Check domain directory exists
if (fs.existsSync(domainPath)) {
log.success(`✓ Domain directory exists: ${domain}/`);
} else {
log.error(`✗ Domain directory missing: ${domain}/`);
}
// Check index.js exists
if (fs.existsSync(indexPath)) {
log.success(`✓ Index file exists: ${domain}/index.js`);
} else {
log.error(`✗ Index file missing: ${domain}/index.js`);
}
// Check route files
const routeFiles = Object.values(routes);
routeFiles.forEach((fileName) => {
const filePath = path.join(domainPath, fileName + '.js');
if (fs.existsSync(filePath)) {
log.success(`✓ Route file exists: ${domain}/${fileName}.js`);
} else {
log.warn(`⚠ Route file not found: ${domain}/${fileName}.js`);
}
});
});
// Check main api index
const apiIndexPath = path.join(routesDir, 'index.js');
if (fs.existsSync(apiIndexPath)) {
log.success(`✓ Main API index exists: routes/api/index.js`);
} else {
log.error(`✗ Main API index missing: routes/api/index.js`);
}
}
/**
* Phase 9: Generate migration report
*/
function generateReport() {
log.section('Phase 9: Migration Summary');
const report = `
═══════════════════════════════════════════════════════════════════════════════
API STRUCTURE MIGRATION REPORT
═══════════════════════════════════════════════════════════════════════════════
MIGRATION: v1/v2/v3 → Domain-Based (skydive/cms/herowars)
OLD STRUCTURE:
└── routes/api/
├── v1/ (Skydive routes)
├── v2/ (CMS + misc routes)
└── v3/ (Hero Wars routes)
NEW STRUCTURE:
└── routes/api/
├── skydive/
├── cms/
└── herowars/
ROUTE MAPPINGS:
✓ /api/v1/* → /api/skydive/*
- applications, aeronefs, canopies, dropzones, jumps, pages, profiles, qcm
✓ /api/v2/* → /api/cms/*
- user, articles, comments, product, products, profiles, tags
✓ /api/v3/* → /api/herowars/*
- clans, members, herowars (moved from v2)
FILES CREATED/UPDATED:
✓ src/routes/api/skydive/index.js
✓ src/routes/api/cms/index.js
✓ src/routes/api/herowars/index.js
✓ src/routes/api/index.js
✓ All route files copied to appropriate domains
MANUAL STEPS REQUIRED:
1. Update Swagger/OpenAPI documentation:
- Review MIGRATION_SWAGGER_TODO.md
- Update path prefixes in src/swagger/*.yaml files
- Update paths in src/config/swagger.js if needed
2. Update app.js (if direct version references exist):
- Search for 'v1', 'v2', 'v3' strings
- Replace with domain names where appropriate
3. Update tests and client calls:
- Replace /api/v1/ with /api/skydive/
- Replace /api/v2/ with /api/cms/
- Replace /api/v3/ with /api/herowars/
4. Update any documentation:
- README.md API endpoints section
- API documentation and guides
5. Test all endpoints:
- Verify each domain routes work correctly
- Check authentication/authorization still functions
- Validate error handling
OPTIONAL: Old v1/v2/v3 directories can be removed after verification:
rm -rf src/routes/api/v1
rm -rf src/routes/api/v2
rm -rf src/routes/api/v3
═══════════════════════════════════════════════════════════════════════════════
`;
console.log(report);
if (!dryRun) {
fs.writeFileSync(path.join(projectRoot, 'MIGRATION_REPORT.md'), report);
}
}
/**
* Main execution
*/
function main() {
console.clear();
console.log(`
${colors.cyan}╔══════════════════════════════════════════════════════════╗
║ API STRUCTURE MIGRATION: v1/v2/v3 → Domain-Based ║
║ (skydive / cms / herowars) ║
╚══════════════════════════════════════════════════════════════╝${colors.reset}
`);
if (dryRun) {
log.warn(`DRY RUN MODE - No changes will be made`);
}
try {
createDomainDirectories();
migrateRouteFiles();
createIndexFiles();
updateApiIndex();
updateSwaggerPaths();
createSwaggerYamlFiles();
gitOperations();
validateMigration();
generateReport();
log.success(`\n✓ Migration script completed successfully!\n`);
if (dryRun) {
log.info(`To apply changes, run: node scripts/migrate-to-domain-structure.js\n`);
} else {
log.info(`To rollback changes, run: git reset --soft HEAD~1 && git restore .\n`);
}
} catch (error) {
log.error(`Migration failed: ${error.message}`);
process.exit(1);
}
}
main();
-322
View File
@@ -1,322 +0,0 @@
#!/usr/bin/env node
/**
* Post-Migration Script: Update documentation and references
*
* This script handles post-migration tasks:
* 1. Update Swagger YAML documentation files
* 2. Update app.js if needed
* 3. Update Postman collection files
* 4. Update other references to v1/v2/v3
*
* Usage: node scripts/update-references-post-migration.js
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
blue: '\x1b[34m',
};
const log = {
info: (msg) => console.log(`${colors.cyan}${colors.reset} ${msg}`),
success: (msg) => console.log(`${colors.green}${colors.reset} ${msg}`),
warn: (msg) => console.log(`${colors.yellow}${colors.reset} ${msg}`),
error: (msg) => console.log(`${colors.red}${colors.reset} ${msg}`),
section: (msg) => console.log(`\n${colors.blue}━━━ ${msg} ━━━${colors.reset}\n`),
};
const projectRoot = path.resolve(__dirname, '..');
const swaggerDir = path.join(projectRoot, 'src/swagger');
/**
* Phase 1: Update Swagger YAML files
*/
function updateSwaggerYaml() {
log.section('Phase 1: Updating Swagger YAML files');
const pathMappings = {
'/api/v1': '/api/skydive',
'/api/v2': '/api/cms',
'/api/v3': '/api/herowars',
};
if (!fs.existsSync(swaggerDir)) {
log.warn(`Swagger directory not found: ${swaggerDir}`);
return;
}
const yamlFiles = fs.readdirSync(swaggerDir).filter((f) => f.endsWith('.yaml'));
yamlFiles.forEach((file) => {
const filePath = path.join(swaggerDir, file);
let content = fs.readFileSync(filePath, 'utf8');
let modified = false;
Object.entries(pathMappings).forEach(([oldPath, newPath]) => {
if (content.includes(oldPath)) {
const regex = new RegExp(oldPath, 'g');
content = content.replace(regex, newPath);
modified = true;
}
});
if (modified) {
fs.writeFileSync(filePath, content);
log.success(`Updated: ${file}`);
} else {
log.info(`No changes needed: ${file}`);
}
});
}
/**
* Phase 2: Update Postman collection and environment
*/
function updatePostmanFiles() {
log.section('Phase 2: Updating Postman files');
const testsDir = path.join(projectRoot, 'tests');
const collectionPath = path.join(testsDir, 'adastra-api-tests.postman_collection.json');
const environmentPath = path.join(testsDir, 'adastra-api-tests.postman_environment.json');
const updateJsonFile = (filePath, description) => {
if (!fs.existsSync(filePath)) {
log.warn(`File not found: ${description}`);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
let modified = false;
const pathMappings = [
{ old: '/api/v1/', new: '/api/skydive/' },
{ old: '/api/v2/', new: '/api/cms/' },
{ old: '/api/v3/', new: '/api/herowars/' },
];
pathMappings.forEach(({ old, new: newPath }) => {
if (content.includes(old)) {
const regex = new RegExp(old, 'g');
content = content.replace(regex, newPath);
modified = true;
}
});
if (modified) {
fs.writeFileSync(filePath, content);
log.success(`Updated: ${description}`);
} else {
log.info(`No changes needed: ${description}`);
}
};
updateJsonFile(collectionPath, 'Postman collection');
updateJsonFile(environmentPath, 'Postman environment');
}
/**
* Phase 3: Check and report other references
*/
function findRemainingReferences() {
log.section('Phase 3: Scanning for remaining v1/v2/v3 references');
const patterns = ['/api/v1', '/api/v2', '/api/v3', 'routes.use(\'/v1', 'routes.use(\'/v2', 'routes.use(\'/v3'];
const ignorePatterns = ['node_modules', '.git', 'dist', 'build', 'v1/', 'v2/', 'v3/'];
const filesToCheck = [
'app.js',
'src/config/swagger.js',
'README.md',
'src/app.js',
'.env*',
];
log.info('Checking key files for old path references...\n');
let foundReferences = false;
filesToCheck.forEach((filePattern) => {
// Handle glob patterns like .env*
let filePaths = [];
if (filePattern.includes('*')) {
const glob = path.basename(filePattern);
const dir = path.dirname(filePattern);
const basePath = dir === '.' ? projectRoot : path.join(projectRoot, dir);
if (fs.existsSync(basePath)) {
const files = fs.readdirSync(basePath);
files.forEach((f) => {
if (new RegExp(`^${glob.replace(/\*/g, '.*')}$`).test(f)) {
filePaths.push(path.join(basePath, f));
}
});
}
} else {
const fullPath = filePattern.startsWith('/')
? filePattern
: path.join(projectRoot, filePattern);
if (fs.existsSync(fullPath)) {
filePaths.push(fullPath);
}
}
filePaths.forEach((filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
patterns.forEach((pattern) => {
lines.forEach((line, index) => {
if (line.includes(pattern)) {
foundReferences = true;
const relativePath = path.relative(projectRoot, filePath);
log.warn(`Found in [${relativePath}:${index + 1}]: ${line.trim()}`);
}
});
});
} catch (err) {
// Silently skip files that can't be read
}
});
});
if (!foundReferences) {
log.success('No remaining v1/v2/v3 references found in key files!');
}
}
/**
* Phase 4: Generate update checklist
*/
function generateUpdateChecklist() {
log.section('Phase 4: Final checklist');
const checklist = `
# Post-Migration Update Checklist
## Files Updated by Script ✓
- [x] Swagger YAML files (/api/v1 → /api/skydive, etc.)
- [x] Postman collection and environment files
- [x] API routes structure (new domains created)
## Manual Verification Required
### 1. Test API Endpoints
\`\`\`bash
npm run local # Start the server
# In another terminal:
curl http://localhost:3200/api-docs # Check Swagger UI
\`\`\`
### 2. Verify Routes
- [ ] GET /api/skydive/jumps
- [ ] GET /api/cms/articles
- [ ] GET /api/herowars/clans
- [ ] Other domain-specific endpoints
### 3. Frontend Updates Required (if applicable)
- [ ] Update Angular environment files (environment.ts, environment.development.ts, etc.)
- [ ] Replace all API calls from /api/v1, /api/v2, /api/v3 to new domains
- [ ] Update service layer URLs
- [ ] Update HTTP interceptors if needed
### 4. Documentation Updates
- [ ] Update README.md with new API structure
- [ ] Update API documentation (if any)
- [ ] Update deployment/infrastructure docs
- [ ] Update team wiki/documentation
### 5. CI/CD Pipeline (if applicable)
- [ ] Update build/deployment scripts referencing v1/v2/v3
- [ ] Update any health checks pointing to old paths
- [ ] Update monitoring/alerting rules
### 6. Client Applications
- [ ] Update all client libraries/SDKs
- [ ] Update mobile app API endpoints
- [ ] Update third-party integrations
### 7. Database/Caching (if applicable)
- [ ] Clear any cached references to old endpoints
- [ ] Update API documentation in databases
- [ ] Check for hardcoded URLs in configuration
## Git Operations
\`\`\`bash
# View the migration commits
git log --oneline -5
# If rollback needed:
git revert <commit-hash>
# Push changes
git push origin develop
\`\`\`
## Optional: Cleanup Old Directories
\`\`\`bash
# After thorough testing, remove old v1/v2/v3 directories:
rm -rf src/routes/api/v1
rm -rf src/routes/api/v2
rm -rf src/routes/api/v3
git add -A
git commit -m "chore: remove old v1/v2/v3 route directories"
\`\`\`
## Rollback Procedure
If issues arise, rollback with:
\`\`\`bash
git reset --hard HEAD~2 # Adjust commit count if needed
\`\`\`
---
Document generated: ${new Date().toISOString()}
`;
const checklistPath = path.join(projectRoot, 'POST_MIGRATION_CHECKLIST.md');
fs.writeFileSync(checklistPath, checklist);
log.success(`Created: POST_MIGRATION_CHECKLIST.md`);
}
/**
* Main execution
*/
function main() {
console.clear();
console.log(`
${colors.cyan}╔══════════════════════════════════════════════════════════╗
║ POST-MIGRATION: Update References & Documentation ║
║ Domain-Based API Structure (skydive/cms/herowars) ║
╚══════════════════════════════════════════════════════════════════╝${colors.reset}
`);
try {
updateSwaggerYaml();
updatePostmanFiles();
findRemainingReferences();
generateUpdateChecklist();
log.success(`\n✓ Post-migration updates completed!\n`);
log.info(`Next steps:
1. Review POST_MIGRATION_CHECKLIST.md
2. Run the server: npm run local
3. Test endpoints in Swagger UI
4. Update frontend API calls
5. Run tests and verify everything works
\n`);
} catch (error) {
log.error(`Post-migration update failed: ${error.message}`);
process.exit(1);
}
}
main();
+132
View File
@@ -0,0 +1,132 @@
var appEnv = process.env.APP_ENV;
if (appEnv == undefined) {
appEnv = 'development';
}
require('dotenv').config({ path: `.env.${appEnv}` });
const { promisify } = require('util');
const express = require("express");
const sequelize = require("./util/database");
const chalk = require('chalk');
const morgan = require("morgan");
const colors = require("colors");
const utils = require('./routes/utils');
const { errorHandler } = require("./middlewares/errorHandler");
var isProduction = appEnv === 'production';
const app = express();
// Body parser
app.use(express.json());
if (process.env.NODE_ENV === "development") {
morgan.token('statusColor', (req, res) => {
let status = (typeof res.headersSent !== 'boolean' ? Boolean(res.header) : res.headersSent)
? res.statusCode
: undefined
return status >= 500 ? chalk.red(status)
: status >= 429 ? chalk.magenta(status)
: status >= 400 ? chalk.yellow(status)
: status >= 300 ? chalk.cyan(status)
: status >= 200 ? chalk.green(status)
: chalk.underline(status);
});
app.use(morgan('[:date[iso]] ":method :url HTTP/:http-version" :statusColor :res[content-length] ":referrer" ":user-agent" :remote-addr'));
//app.use(require('method-override')());
//app.use(express.static(__dirname + '/public'));
} else {
app.use(morgan("dev"));
}
// CORS
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
);
if (req.method === "OPTIONS") {
res.header("Access-Control-Allow-Methods", "PUT, POST, PATCH, DELETE, GET");
return res.status(200).json({});
}
next();
});
/*
var db = require("./models");
*/
// Import Models
const User = require("./models/User");
const Article = require("./models/Article");
const Tag = require("./models/Tag");
const Comment = require("./models/Comment");
// Relations
User.belongsToMany(User, { as: "followers", through: "Followers", foreignKey: "userId", timestamps: false });
User.belongsToMany(User, { as: "following", through: "Followers", foreignKey: "followerId", timestamps: false });
User.hasMany(Article, { foreignKey: "authorId", onDelete: "CASCADE" });
Article.belongsTo(User, { as: "author", foreignKey: "authorId" });
User.hasMany(Comment, { foreignKey: "authorId", onDelete: "CASCADE" });
Comment.belongsTo(User, { as: "author", foreignKey: "authorId" });
Article.hasMany(Comment, { foreignKey: "articleId", onDelete: "CASCADE" });
Comment.belongsTo(Article, { foreignKey: "articleId" });
User.belongsToMany(Article, { as: "favorites", through: "Favorites", timestamps: false });
Article.belongsToMany(User, { through: "Favorites", foreignKey: "articleId", timestamps: false });
Article.belongsToMany(Tag, { through: "TagLists", as: "tagLists", foreignKey: "articleId", timestamps: false, onDelete: "CASCADE" });
Tag.belongsToMany(Article, { through: "ArticleTags", uniqueKey: false, timestamps: false });
// Route files
const users = require("./routes/api/users");
const profiles = require("./routes/api/profiles");
const articles = require("./routes/api/articles");
const comments = require("./routes/api/comments");
const tags = require("./routes/api/tags");
// Mount routers
app.use(users);
app.use(profiles);
app.use(articles);
app.use(comments);
app.use(tags);
//app.use(require('./routes'));
app.use(errorHandler);
//console.log(`User: ${User}`);
//Article.sync();
/*
const sync = async () => await sequelize.sync({ force: true });
sync().then(() => {
User.create({
email: "jgautier.webdev@gmail.com",
username: "adastra",
firstname: "Julien",
lastname: "Gautier",
password: "DKxp24PSnr",
role: "admin"
});
User.create({
email: "rampeur@gmail.com",
username: "solide",
firstname: "Rampeur",
lastname: "Solide",
password: "DKxp24PSnr",
role: "user"
});
});
*/
const startServer = async () => {
const port = process.env.SERVER_PORT || 3200;
await promisify(app.listen).bind(app)(port);
console.log(`${utils.getDateTimeISOString()}`, chalk.green(`${process.env.APP_ENV} API server listening on port ${port}`));
}
// finally, let's start our server...
startServer();
-27
View File
@@ -1,27 +0,0 @@
const dotenv = require('dotenv');
dotenv.config();
// configuration for the database connection based on the environment (development, test, production)
module.exports = {
development: {
use_env_variable: 'MYSQL_DATABASE_URL_DEV',
},
test: {
use_env_variable: 'MYSQL_DATABASE_URL_DEV',
dialect: "mysql",
dialectOptions: {
ssl: process.env.NODE_ENV === 'production', // set this value based on your environment
},
},
production: {
use_env_variable: 'MYSQL_DATABASE_URL',
dialectOptions: {
ssl: {
// enable this for production environment only if using a secure connection
require: true,
rejectUnauthorized: false,
},
},
},
};
-19
View File
@@ -1,19 +0,0 @@
if (!process.env.SECRET) {
throw new Error('SECRET environment variable is required and must not be empty');
}
const config = {
secret: process.env.SECRET,
session_lifetime: parseInt(process.env.SESSION_LIFETIME) || 21600, // 21600 secondes = 6 heures
initAuth: () => {
require('./passport-local');
},
initApiKey: () => {
require('./passport-headerapikey');
}
};
module.exports = config;
-20
View File
@@ -1,20 +0,0 @@
var secret = require('.').secret;
const crypto = require('crypto');
const passport = require('passport');
var HeaderAPIKeyStrategy = require('passport-headerapikey').HeaderAPIKeyStrategy;
const DB = require('../database/mysql');
passport.use('headerapikey', new HeaderAPIKeyStrategy(
{ header: 'Authorization', prefix: `${process.env.AUTHORIZATION_PREFIX} ` },
false,
function (apikey, done) {
const encryptedKey = crypto.createHmac('sha256', secret).update(apikey).digest('hex');
DB.Application.findOne({ where: { apikey: encryptedKey } })
.then(function (application) {
if (!application) {
return done({ message: "Application can't be found.", status: 404 }, false, false);
}
return done(null, application);
}).catch(done);
}
));
-17
View File
@@ -1,17 +0,0 @@
const passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
const { UserService } = require('../services');
passport.use('local', new LocalStrategy({
usernameField: 'user[email]',
passwordField: 'user[password]'
}, function (email, password, done) {
UserService.getUserByEmail(email).then(function (user) {
//User.findOne({ email: email }).then(function (user) {
if (!user || !user.validPassword(password)) {
return done(null, false, { errors: { 'email ou mot de passe': 'invalide' } });
}
return done(null, user);
}).catch(done);
}));
-130
View File
@@ -1,130 +0,0 @@
const swaggerJsdoc = require('swagger-jsdoc');
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
// Fonction pour charger tous les fichiers YAML du dossier swagger
const loadSwaggerFiles = () => {
const swaggerDir = path.join(__dirname, '../swagger');
const files = fs.readdirSync(swaggerDir).filter(file => file.endsWith('.yaml'));
const merged = { paths: {}, components: { schemas: {} } };
files.forEach(file => {
try {
const content = fs.readFileSync(path.join(swaggerDir, file), 'utf8');
const doc = yaml.load(content);
// Fusionner les paths
if (doc.paths) {
Object.assign(merged.paths, doc.paths);
}
// Fusionner les schemas
if (doc.components && doc.components.schemas) {
Object.assign(merged.components.schemas, doc.components.schemas);
}
} catch (error) {
console.error(`Erreur lors du chargement de ${file}:`, error);
}
});
return merged;
};
const swaggerDefinitions = loadSwaggerFiles();
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Adastra API',
version: '1.0.0',
description: 'Adastra API Documentation - Express.js API with Sequelize (MySQL) and Mongoose (MongoDB)',
contact: {
name: 'Julien Gautier',
email: 'jgautier.webdev@gmail.com',
},
},
servers: [
{
url: 'http://localhost:3201',
description: 'Development server',
},
{
url: 'http://localhost:3200',
description: 'Local development server',
},
{
url: 'https://api.example.com',
description: 'Production server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT Bearer token authentication',
},
apiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'x-api-key',
description: 'API Key authentication',
},
},
schemas: {
Error: {
type: 'object',
properties: {
error: {
type: 'string',
},
message: {
type: 'string',
},
statusCode: {
type: 'integer',
},
},
},
...swaggerDefinitions.components.schemas,
},
},
paths: swaggerDefinitions.paths,
tags: [
{
name: 'Articles',
description: 'Article management endpoints',
},
{
name: 'Clans',
description: 'Clan management endpoints',
},
{
name: 'Members',
description: 'Member management endpoints',
},
{
name: 'Users',
description: 'User management endpoints',
},
{
name: 'Products',
description: 'Product management endpoints',
},
{
name: 'Hero Wars',
description: 'Hero Wars game data endpoints',
},
],
},
// Les définitions sont chargées depuis les fichiers YAML
apis: [],
};
const swaggerSpec = swaggerJsdoc(options);
module.exports = swaggerSpec;
-251
View File
@@ -1,251 +0,0 @@
const { ArticleService, TagService, UserService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
const includeOptions = ArticleService.getArticleIncludeAssoc();
module.exports.createArticle = async (req, res, next) => {
try {
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
const { title, description, body, tagList } = req.body.article;
const articleSlug = ArticleService.slugify(title);
const slugInDB = await ArticleService.isSlugUsed(articleSlug);
if (slugInDB) {
return next(new ErrorResponse("Slug already exists", 400));
}
const article = await ArticleService.createArticle({
slug: articleSlug,
title: title,
description: description,
body: body,
});
for (const tag of tagList) {
const tagInDB = await TagService.getTagByPk(tag.trim());
if (tagInDB) {
await article.addArticleTag(tagInDB);
} else if (tag.length > 2) {
const newTag = await TagService.createTag(tag.trim());
await article.addArticleTag(newTag);
}
}
delete loggedUser.dataValues.token;
article.dataValues.tagList = tagList;
article.setAuthor(loggedUser);
article.dataValues.author = loggedUser;
await ArticleService.appendFollowers(loggedUser, loggedUser);
await ArticleService.appendFavorites(loggedUser, article);
res.status(201).json({
message: 'Article created successfully.',
data: article.toJSONFor(),
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while creating article.', 500, err.message));
}
};
module.exports.deleteArticle = async (req, res, next) => {
try {
const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
const article = await ArticleService.getArticleBySlug(slug);
if (!article) {
return next(new ErrorResponse("Article not found", 404));
}
if (article.authorId !== loggedUser.id) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
let data = await ArticleService.deleteArticle(article);
res.status(200).json({
message: 'Article deleted successfully.',
data: data.slug
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while deleting article.', 500, err.message));
}
};
module.exports.getArticle = async (req, res, next) => {
try {
const { slug } = req.params;
let article = await ArticleService.getArticleBySlugWithTagList(slug);
if (!article) {
return next(new ErrorResponse("Article not found", 404));
}
const loggedUser = await UserService.getLoggedUserById(req.payload?.id || null);
//await ArticleService.appendTagList(article);
await ArticleService.appendFollowers(loggedUser, article);
await ArticleService.appendFavorites(loggedUser, article);
res.status(200).json({ article: article.toJSONFor() });
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching article.', 500, err.message));
}
};
module.exports.getArticles = async (req, res, next) => {
try {
let articles = { rows: [], count: 0 };
const { tag, author, favorited, limit = 20, offset = 0 } = req.query;
const loggedUser = await UserService.getLoggedUserById(req.payload?.id || null);
const searchOptions = await ArticleService.getSearchOptionsArticles(tag, author, limit, offset);
if (favorited) {
const user = await UserService.getUserByUsername(favorited);
if (!user) {
return next(new ErrorResponse("User not found", 404));
}
articles.rows = await UserService.getFavorites(user, searchOptions);
articles.count = await UserService.getFavoritesCount(user);
} else {
articles = await ArticleService.getAllArticlesAndCount(searchOptions);
}
for (let article of articles.rows) {
await ArticleService.appendTagList(article);
await ArticleService.appendFollowers(loggedUser, article);
await ArticleService.appendFavorites(loggedUser, article);
}
const data = articles.rows.map((article) => {
return article.toJSONFor();
});
res.status(200).json({
data: {
articles: data,
articlesCount: articles.count
}
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching articles.', 500, err.message));
}
};
module.exports.updateArticle = async (req, res, next) => {
try {
const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
const article = await ArticleService.getArticleBySlug(slug);
if (!article) {
return next(new ErrorResponse("Article not found", 404));
}
if (article.authorId !== loggedUser.id) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
const { title, description, body } = req.body.article;
const articleSlug = ArticleService.slugify(title ? title : article.title);
const slugInDB = await ArticleService.isSlugUsed(articleSlug);
if (slugInDB && articleSlug !== slug) {
return next(new ErrorResponse("Title already exists", 400));
}
//await article.update({
await ArticleService.updateArticleById({
slug: articleSlug ? articleSlug : article.slug,
title: title ? title : article.title,
description: description ? description : article.description,
body: body ? body : article.body,
}, article.id);
const update = await ArticleService.getArticleByIdWithTagList(article.id);
await ArticleService.appendFollowers(loggedUser, update);
await ArticleService.appendFavorites(loggedUser, update);
res.status(200).json({
message: 'Article updated successfully.',
article: update.toJSONFor()
//article: article
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while updating article.', 500, err.message));
}
};
module.exports.articlesFeed = async (req, res, next) => {
try {
const { limit = 3, offset = 0 } = req.query;
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
const authors = await UserService.getUserFollowed(loggedUser);
const searchOptions = {
include: includeOptions,
limit: parseInt(limit),
offset: offset * limit,
order: [["createdAt", "DESC"]],
where: { authorId: authors.map((author) => author.id) },
distinct: true,
}
const articles = await ArticleService.getAllArticlesAndCount(searchOptions);
for (const article of articles.rows) {
await ArticleService.appendTagList(article);
await ArticleService.appendFollowers(loggedUser, article);
await ArticleService.appendFavorites(loggedUser, article);
}
res.status(200).json({
data: {
articles: articles.rows,
articlesCount: articles.count
}
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching articles feed.', 500, err.message));
}
};
module.exports.addFavoriteArticle = async (req, res, next) => {
try {
const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
const article = await ArticleService.getArticleBySlugWithTagList(slug);
if (!article) {
return next(new ErrorResponse("Article not found", 404));
}
await UserService.addFavoriteArticle(loggedUser, article);
await ArticleService.appendFollowers(loggedUser, article);
await ArticleService.appendFavorites(loggedUser, article);
res.status(200).json({ article });
} catch (err) {
return next(new ErrorResponse('Something went wrong while adding a favorite.', 500, err.message));
}
};
module.exports.deleteFavoriteArticle = async (req, res, next) => {
try {
const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
const article = await ArticleService.getArticleBySlugWithTagList(slug);
if (!article) {
return next(new ErrorResponse("Article not found", 404));
}
await UserService.removeFavoriteArticle(loggedUser, article);
await ArticleService.appendFollowers(loggedUser, article);
await ArticleService.appendFavorites(loggedUser, article);
res.status(200).json({ article });
} catch (err) {
return next(new ErrorResponse('Something went wrong while removing a favorite.', 500, err.message));
}
};
-112
View File
@@ -1,112 +0,0 @@
const { ClanService, UserService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
module.exports.createClan = async (req, res, next) => {
try {
const user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
const {
id, title, country, description, disbanding, frameId, icon, level, members,
membersCount, minLevel, ownerId, serverId, topActivity, topDungeon
} = req.body;
const author = user.id;
const clan = await ClanService.createClan({
id, title, country, description, disbanding, frameId, icon, level, members,
membersCount, minLevel, ownerId, serverId, topActivity, topDungeon, author
});
//console.log(clan);
res.status(201).json({
message: 'Clan created successfully.',
data: clan.toJSONFor(),
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while creating clan.`, 500, err.message));
}
};
module.exports.deleteClan = async (req, res, next) => {
try {
const { clanId } = req.params;
let clan = await ClanService.getClanById(clanId);
if (req.payload.id !== clan.author.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
let data = await ClanService.deleteClan(clan);
res.status(200).json({
message: 'Clan deleted successfully.',
data: data.id
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while deleting clan.`, 500, err.message));
}
};
module.exports.getAllClans = async (req, res, next) => {
try {
let query = {};
let { limit = 25, offset = 0 } = req.query;
if (typeof req.query.title !== 'undefined') {
const rgx = new RegExp(`.*${req.query.title}.*`);
query = {
$or: [
{ title: { $regex: rgx, $options: "i" } },
{ id: { $regex: rgx, $options: "i" } },
],
}
}
const data = await ClanService.getAllClans(query, limit, offset);
res.status(200).json({
data: {
clans: data.clans.map(function (clan) {
return clan.toJSONFor();
}),
clansCount: data.clansCount
}
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while fetching clans.`, 500, err.message));
}
};
module.exports.getClan = async (req, res, next) => {
try {
const { clanId } = req.params;
let clan = await ClanService.getClanById(clanId);
if (!clan) {
return next(new ErrorResponse("Clan not found", 404));
}
res.status(200).json({ clan: clan.toJSONFor() });
} catch (err) {
return next(new ErrorResponse(`Something went wrong while fetching clan.`, 500, err.message));
}
};
module.exports.updateClan = async (req, res, next) => {
try {
const { clanId } = req.params;
let clan = await ClanService.getClanById(clanId);
console.log(clan.author.id, clan.author);
if (req.payload.id !== clan.author.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
const UPDATABLE_FIELDS = ['country', 'description', 'disbanding', 'frameId', 'flag', 'level', 'membersCount', 'minLevel', 'ownerId', 'serverId', 'title', 'topActivity', 'topDungeon'];
UPDATABLE_FIELDS.forEach(field => {
if (typeof req.body.clan[field] !== 'undefined') clan[field] = req.body.clan[field];
});
let data = await ClanService.updateClan(clan);
res.status(200).json({
message: 'Clan updated successfully.',
clan: data.toJSONFor()
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while updating clan.`, 500, err.message));
}
};
-34
View File
@@ -1,34 +0,0 @@
const { CommentService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
module.exports.createComment = async (req, res, next) => {
try {
const { slug, title, description, body } = req.body;
const comment = await CommentService.createComment({
slug,
title,
description,
body,
authorId: req.payload.id
});
res.status(201).json({
message: 'Comment created successfully.',
data: comment,
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while creating comment.', 500, err.message));
}
};
module.exports.getAllComments = async (req, res, next) => {
try {
const comments = await CommentService.getAllComments();
res.status(200).json({
data: comments,
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching comments.', 500, err.message));
}
};
-111
View File
@@ -1,111 +0,0 @@
const { MemberService, UserService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
module.exports.createMember = async (req, res, next) => {
try {
const user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
const {
id, allowPm, avatarId, clanIcon, clanId, clanRole, clanTitle, commander, frameId,
isChatModerator, lastLoginTime, leagueId, level, name, serverId
} = req.body;
const author = user.id;
const member = await MemberService.createMember({
id, allowPm, avatarId, clanIcon, clanId, clanRole, clanTitle, commander, frameId,
isChatModerator, lastLoginTime, leagueId, level, name, serverId, author
});
res.status(201).json({
message: 'Member created successfully.',
data: member.toJSONFor(),
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while creating member.`, 500, err.message));
}
};
module.exports.deleteMember = async (req, res, next) => {
try {
const { memberId } = req.params;
let member = await MemberService.getMemberById(memberId);
if (req.payload.id !== member.author.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
let data = await MemberService.deleteMember(member);
res.status(200).json({
message: 'Member deleted successfully.',
data: data.id
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while deleting member.`, 500, err.message));
}
};
module.exports.getAllMembers = async (req, res, next) => {
try {
let query = {};
let { limit = 25, offset = 0 } = req.query;
if (typeof req.query.name !== 'undefined') {
const rgx = new RegExp(`.*${req.query.name}.*`);
query = {
$or: [
{ name: { $regex: rgx, $options: "i" } },
{ id: { $regex: rgx, $options: "i" } },
],
}
}
const data = await MemberService.getAllMembers(query, limit, offset);
res.status(200).json({
data: {
members: data.members.map(function (member) {
return member.toJSONFor();
}),
membersCount: data.membersCount
}
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while fetching members.`, 500, err.message));
}
};
module.exports.getMember = async (req, res, next) => {
try {
const { memberId } = req.params;
let member = await MemberService.getMemberById(memberId);
if (!member) {
return next(new ErrorResponse("Member not found", 404));
}
res.status(200).json({ member: member.toJSONFor() });
} catch (err) {
return next(new ErrorResponse(`Something went wrong while updating member.`, 500, err.message));
}
};
module.exports.updateMember = async (req, res, next) => {
try {
const { memberId } = req.params;
let member = await MemberService.getMemberById(memberId);
if (req.payload.id !== member.author.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
const UPDATABLE_FIELDS = ['name', 'lastLoginTime', 'serverId', 'level', 'clanId', 'clanRole', 'commander', 'avatarId', 'isChatModerator', 'frameId', 'leagueId', 'clanTitle', 'champion', 'stats', 'membership', 'adventureSum', 'clanGiftsSum', 'clanWarSum', 'history', 'score', 'scoreGifts', 'warGifts', 'rewards'];
UPDATABLE_FIELDS.forEach(field => {
if (typeof req.body.member[field] !== 'undefined') member[field] = req.body.member[field];
});
let data = await MemberService.updateMember(member);
res.status(200).json({
message: 'Member updated successfully.',
member: data.toJSONFor()
});
} catch (err) {
return next(new ErrorResponse(`Something went wrong while updating member.`, 500, err.message));
}
};
-274
View File
@@ -1,274 +0,0 @@
const { ProductService } = require('../services');
//const { where } = require("sequelize");
const Product = require("../database/models/mysql/Product");
const Tag = require("../database/models/mysql/Tag");
const User = require("../database/models/mysql/User");
const ErrorResponse = require("../utils/errorResponse");
const slug = require("slug");
const {
appendFollowers,
appendFavorites,
appendTagList,
} = require("../utils/helpers");
const includeOptions = [
{
model: Tag,
as: "tagLists",
attributes: ["name"],
through: { attributes: [] },
},
{ model: User, as: "author", attributes: { exclude: ["email", "password"] } },
];
module.exports.getProducts = async (req, res) => {
const { tag, author, favorited, limit = 20, offset = 0 } = req.query;
const { loggedUser } = req;
const searchOptions = {
include: [
{
model: Tag,
as: "tagLists",
attributes: ["name"],
through: { attributes: [] }, // ? this will remove the rows from the join table
// where: tag ? { name: tag } : {},
...(tag && { where: { name: tag } }),
},
{
model: User,
as: "author",
attributes: { exclude: ["password", "email"] },
// where: author ? { username: author } : {},
...(author && { where: { username: author } }),
},
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [["createdAt", "DESC"]],
distinct: true,
};
let products = { rows: [], count: 0 };
if (favorited) {
const user = await User.findOne({ where: { username: favorited } });
products.rows = await user.getFavorites(searchOptions);
products.count = await user.countFavorites();
} else {
products = await Product.findAndCountAll(searchOptions);
}
for (let product of products.rows) {
const productTags = await product.getTagLists();
// const productTags = product.tagLists;
appendTagList(productTags, product);
await appendFollowers(loggedUser, product);
await appendFavorites(loggedUser, product);
delete product.dataValues.Favorites;
}
res
.status(200)
.json({ products: products.rows, productsCount: products.count });
};
module.exports.getProductsByCategory = async (req, res, next) => {
const { category } = req.params;
//console.log(category);
let products = await ProductService.getProductsByCategory(category);
//console.log(products);
if (!products) {
return next(new ErrorResponse("Products not found", 404));
}
res.status(200).json({ category: category, count: products.count, products: products.rows });
};
module.exports.productsFeed = async (req, res) => {
const { loggedUser } = req;
const { limit = 3, offset = 0 } = req.query;
const authors = await loggedUser.getFollowing();
const products = await Product.findAndCountAll({
include: includeOptions,
limit: parseInt(limit),
offset: offset * limit,
order: [["createdAt", "DESC"]],
where: { authorId: authors.map((author) => author.id) },
distinct: true,
});
for (const product of products.rows) {
const productTags = await product.getTagLists();
appendTagList(productTags, product);
await appendFollowers(loggedUser, product);
await appendFavorites(loggedUser, product);
}
res.json({ products: products.rows, productsCount: products.count });
};
module.exports.getProduct = async (req, res, next) => {
const { slug: productSlug } = req.params;
let product = await ProductService.getProductBySlug(productSlug);
if (!product) {
return next(new ErrorResponse("Product not found", 404));
}
let tags = product.productTags;
product = product.toJSONFor();
product.tags = tags;
res.status(200).json({ product: product });
};
module.exports.createProduct = async (req, res, next) => {
const { loggedUser } = req;
const { title, description, body, tagList } = req.body.product;
const slugInDB = await Product.findOne({ where: { slug: slug(`${title}`) } });
if (slugInDB) {
return next(new ErrorResponse("Title already exists", 400));
}
const product = await Product.create({
title: title,
description: description,
body: body,
});
for (const tag of tagList) {
const tagInDB = await Tag.findByPk(tag.trim());
if (tagInDB) {
await product.addProductTag(tagInDB);
} else if (tag.length > 2) {
const newTag = await Tag.create({ name: tag.trim() });
await product.addProductTag(newTag);
}
}
delete loggedUser.dataValues.token;
product.dataValues.tagList = tagList;
product.setAuthor(loggedUser);
product.dataValues.author = loggedUser;
await appendFollowers(loggedUser, loggedUser);
await appendFavorites(loggedUser, product);
res.status(201).json({ product });
};
module.exports.deleteProduct = async (req, res, next) => {
const { slug: productSlug } = req.params;
const { loggedUser } = req;
const product = await Product.findOne({
where: { slug: productSlug },
include: includeOptions,
});
if (!product) {
return next(new ErrorResponse("Product not found", 404));
}
if (product.authorId !== loggedUser.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
await product.destroy();
res.status(200).json({ product });
};
module.exports.updateProduct = async (req, res, next) => {
const { slug: productSlug } = req.params;
const { loggedUser } = req;
const product = await Product.findOne({
where: { slug: productSlug },
include: includeOptions,
});
if (!product) {
return next(new ErrorResponse("Product not found", 404));
}
if (product.authorId !== loggedUser.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
const { title, description, body } = req.body.product;
const slugInDB = await Product.findOne({
where: { slug: slug(title ? title : product.title) },
});
if (slugInDB && slugInDB.slug !== productSlug) {
return next(new ErrorResponse("Title already exists", 400));
}
await product.update({
title: title ? title : product.title,
description: description ? description : product.description,
body: body ? body : product.body,
});
const productTags = await product.getTagLists();
appendTagList(productTags, product);
await appendFollowers(loggedUser, product);
await appendFavorites(loggedUser, product);
res.status(200).json({ product });
};
module.exports.addFavoriteProduct = async (req, res, next) => {
const { loggedUser } = req;
const { slug: productSlug } = req.params;
const product = await Product.findOne({
where: { slug: productSlug },
include: includeOptions,
});
if (!product) {
return next(new ErrorResponse("Product not found", 404));
}
await loggedUser.addFavorite(product);
const productTags = await product.getTagLists();
appendTagList(productTags, product);
await appendFollowers(loggedUser, product);
await appendFavorites(loggedUser, product);
res.status(200).json({ product });
};
module.exports.deleteFavoriteProduct = async (req, res, next) => {
const { loggedUser } = req;
const { slug: productSlug } = req.params;
const product = await Product.findOne({
where: { slug: productSlug },
include: includeOptions,
});
if (!product) {
return next(new ErrorResponse("Product not found", 404));
}
await loggedUser.removeFavorite(product);
const productTags = await product.getTagLists();
appendTagList(productTags, product);
await appendFollowers(loggedUser, product);
await appendFavorites(loggedUser, product);
res.status(200).json({ product });
};
-50
View File
@@ -1,50 +0,0 @@
const { UserService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
module.exports.addFollowUser = async (req, res, next) => {
try {
return res.status(200).json({
message: 'The user is now being followed.'
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while following a user.', 500, err.message));
}
};
module.exports.deleteFollowUser = async (req, res, next) => {
try {
return res.status(200).json({
message: 'The user is no longer being followed.',
user: true
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while unfollowing a user.', 500, err.message));
}
};
module.exports.getProfile = async (req, res, next) => {
try {
/*const user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}*/
res.status(200).json({
user: req.profile.toProfileJSONFor()
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
}
};
module.exports.getParamUsername = async (req, res, next, value) => {
try {
const user = await UserService.getUserByUsername(value);
if (!user) {
return next(new ErrorResponse("User not found", 404));
}
req.profile = user;
return next();
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
}
};
-59
View File
@@ -1,59 +0,0 @@
const { TagService, UserService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
module.exports.createTag = async (req, res, next) => {
try {
const { name } = req.body.tag;
const tagInDB = await TagService.tagIsInDB(name);
if (tagInDB) {
return next(new ErrorResponse("Tag already exists", 400));
}
const tag = await TagService.createTag(name);
res.status(201).json({
message: 'Tag created successfully.',
data: tag.toJSONFor(),
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while creating tag.', 500, err.message));
}
};
module.exports.deleteTag = async (req, res, next) => {
try {
const { name } = req.params;
const tag = await TagService.getTagByName(name);
if (!tag) {
return next(new ErrorResponse("Tag not found", 404));
}
const loggedUser = await UserService.getLoggedUserById(req.payload.id);
if (loggedUser.role !== "Admin") {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
let data = await TagService.deleteTag(tag);
res.status(200).json({
message: 'Tag deleted successfully.',
data: data.name
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching tags.', 500, err.message));
}
};
module.exports.getAllTags = async (req, res, next) => {
try {
const tags = await TagService.getAllTags();
res.status(200).json({
data: {
tags: tags,
tagsCount: tags.length
}
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching tags.', 500, err.message));
}
};
-233
View File
@@ -1,233 +0,0 @@
const { UserService, SkydiverProfileService } = require('../services');
const ErrorResponse = require("../utils/errorResponse");
var crypto = require('crypto');
const passport = require('passport');
module.exports.authenticate = async (req, res, next) => {
try {
passport.authenticate('headerapikey', { session: false }, function (err, application, info) {
if (err) {
//return res.status(err.status || 500).json({message: "An authentication error occured.", err: err.message});
return next(new ErrorResponse('Something went wrong while authenticating.', err.status || 500, err.message));
}
if (!application) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
if (application.author) {
application.author.token = application.author.generateJWT();
return res.json({
user: application.author.toAuthJSON(),
application: application.toAuthJSON()
});
} else {
return res.status(422).json(info);
}
})(req, res, next);
} catch (err) {
//res.status(500).json({ message: 'Something went wrong while authenticating.', error: err.message });
return next(new ErrorResponse('Something went wrong while authenticating.', 500, err.message));
}
};
module.exports.createUser = async (req, res, next) => {
try {
const { username, firstname, lastname, phone, email, password } = req.body.user;
const userSlug = UserService.slugify(username);
const slugInDB = await UserService.isUsernameUsed(userSlug);
if (slugInDB) {
return next(new ErrorResponse("Username already exists", 400));
}
const uuid = crypto.randomUUID()
const user = await UserService.createUser({
id: uuid,
username,
firstname,
lastname,
phone,
email,
password
});
res.status(201).json({
message: 'User created successfully.',
data: user,
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while creating user.', 500, err.message));
}
};
module.exports.getAllUsers = async (req, res, next) => {
try {
const users = await UserService.getAllUsers();
res.status(200).json({
data: users,
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
}
};
module.exports.getUser = async (req, res, next) => {
try {
const user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
}
const profile = await SkydiverProfileService.getByUserId(req.payload.id);
res.status(200).json({
user: {
...user.toAuthJSON(),
...(profile ? profile.toJSONFor() : { poids: null, licence: null, bg_image: null }),
}
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
}
};
module.exports.loginAsUser = async (req, res, next) => {
try {
passport.authenticate('local', { session: false }, function (err, user, info) {
if (err) {
return next(err);
}
if (user) {
user.token = user.generateJWT();
return res.json({ user: user.toAuthJSON() });
} else {
return res.status(422).json(info);
}
})(req, res, next);
} catch (err) {
return next(new ErrorResponse('Something went wrong while processing login.', 500, err.message));
}
};
module.exports.updateUser = async (req, res, next) => {
try {
let user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
if (typeof req.body.user.firstname !== 'undefined') {
user.firstname = req.body.user.firstname;
}
if (typeof req.body.user.lastname !== 'undefined') {
user.lastname = req.body.user.lastname;
}
if (typeof req.body.user.phone !== 'undefined') {
user.phone = req.body.user.phone;
}
if (typeof req.body.user.image !== 'undefined') {
user.image = req.body.user.image;
}
if (typeof req.body.user.role !== 'undefined') {
return next(new ErrorResponse("Forbidden", 403, "A suspected attempt to usurp rights has been detected. The suspicious activity has been reported and xill be investigated."));
}
const updatedUser = await UserService.updateUserById(user, req.payload.id);
const skydiverFields = {};
if (typeof req.body.user.licence !== 'undefined') skydiverFields.licence = req.body.user.licence;
if (typeof req.body.user.poids !== 'undefined') skydiverFields.poids = req.body.user.poids;
if (typeof req.body.user.bg_image !== 'undefined') skydiverFields.bg_image = req.body.user.bg_image;
if (Object.keys(skydiverFields).length > 0) {
await SkydiverProfileService.upsert(req.payload.id, skydiverFields);
}
res.status(200).json({
user: updatedUser.toAuthJSON()
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
}
};
module.exports.updateUserEmail = async (req, res, next) => {
try {
let user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
if (typeof req.body.user.email !== 'undefined') {
user.email = req.body.user.email;
}
user = await UserService.updateUserById(user, req.payload.id);
res.status(200).json({
user: user.toAuthJSON(),
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
}
};
module.exports.updateUserPassword = async (req, res, next) => {
try {
let user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
if (typeof req.body.user.password !== 'undefined') {
const {salt, hash} = UserService.generateSaltHash(req.body.user.password);
user.salt = salt;
user.hash = hash;
}
user = await UserService.updateUserById(user, req.payload.id);
res.status(200).json({
user: user.toAuthJSON(),
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
}
};
module.exports.updateUserRole = async (req, res, next) => {
try {
let user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
if (user.role !== 'Admin') {
return next(new ErrorResponse("Forbidden", 403, "You are not allowed to change user roles."));
}
if (typeof req.body.user.role !== 'undefined') {
user.role = req.body.user.role;
}
user = await UserService.updateUserById(user, req.payload.id);
res.status(200).json({
user: user.toAuthJSON(),
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
}
};
module.exports.updateUserUsername = async (req, res, next) => {
try {
let user = await UserService.getUserById(req.payload.id);
if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource."));
}
if (typeof req.body.user.username !== 'undefined') {
user.username = req.body.user.username;
}
user = await UserService.updateUserById(user, req.payload.id);
res.status(200).json({
user: user.toAuthJSON(),
});
} catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
}
};
-40
View File
@@ -1,40 +0,0 @@
var express = require('express'),
session = require('express-session'),
cors = require('cors'),
helmet = require('helmet');
const config = require('./config'),
swaggerUi = require('swagger-ui-express'),
swaggerSpec = require('./config/swagger');
function createApp() {
const app = express();
if (process.env.TRUST_PROXY) {
app.set('trust proxy', parseInt(process.env.TRUST_PROXY, 10) || process.env.TRUST_PROXY);
}
app.use(helmet({ contentSecurityPolicy: false }));
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
: [];
app.use(cors({
origin: allowedOrigins.length > 0 ? allowedOrigins : false,
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(__dirname + '/../public'));
app.use(session({
secret: process.env.SESSION_SECRET || config.secret,
cookie: { maxAge: config.session_lifetime },
resave: false,
saveUninitialized: false
}));
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
swaggerOptions: { persistAuthorization: true },
}));
return app;
}
module.exports = createApp;
-37
View File
@@ -1,37 +0,0 @@
// Load environment variables from .env file
const appEnv = process.env.APP_ENV || 'development';
require('dotenv').config({ path: `config/env/.env.${appEnv}` });
module.exports = {
"development": {
"username": process.env.MYSQL_DBUSER || 'root',
"password": process.env.MYSQL_DBPASS || '',
"database": process.env.MYSQL_DBNAME || 'adastra_dev',
"host": process.env.MYSQL_DBHOST || 'localhost',
"port": process.env.MYSQL_DBPORT || 3306,
"dialect": process.env.MYSQL_DBTYPE || 'mysql'
},
"test": {
"username": process.env.MYSQL_DBUSER || 'root',
"password": process.env.MYSQL_DBPASS || '',
"database": process.env.MYSQL_DBNAME || 'adastra_test',
"host": process.env.MYSQL_DBHOST || 'localhost',
"port": process.env.MYSQL_DBPORT || 3306,
"dialect": process.env.MYSQL_DBTYPE || 'mysql'
},
"production": {
"username": process.env.MYSQL_DBUSER,
"password": process.env.MYSQL_DBPASS,
"database": process.env.MYSQL_DBNAME,
"host": process.env.MYSQL_DBHOST,
"port": process.env.MYSQL_DBPORT,
"dialect": process.env.MYSQL_DBTYPE || 'mysql',
"dialectOptions": {
"ssl": {
// enable this for production environment only if using a secure connection
"require": true,
"rejectUnauthorized": false
},
}
}
}
-43
View File
@@ -1,43 +0,0 @@
const { Sequelize } = require('sequelize');
const Op = Sequelize.Op;
const sequelize = new Sequelize(
process.env.MYSQL_DBNAME,
process.env.MYSQL_DBUSER,
process.env.MYSQL_DBPASS,
{
host: process.env.MYSQL_DBHOST,
port: process.env.MYSQL_DBPORT,
dialect: process.env.MYSQL_DBTYPE,
$like: Op.like,
$not: Op.not,
timezone: '+02:00',
define: {
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
underscored: false,
freezeTableName: true,
},
pool: {
//explain this line of code? this means that the connection pool will have a minimum of 0 connections and a maximum of 5 connections
min: 0,
max: 5,
},
logQueryParameters: process.env.APP_ENV === 'development', // this line of code will log the query parameters if the environment is development
benchmark: true,
});
/*
const checkConnection = async () => {
try {
await sequelize.authenticate();
console.log(`DB Connected`.cyan.underline.bold);
} catch (error) {
console.error("Unable to connect to the database:".red.bold, error);
}
};
checkConnection();
*/
module.exports = sequelize;
-107
View File
@@ -1,107 +0,0 @@
const chalk = require('chalk');
const mongoose = require('mongoose');
const DB = require('./mysql');
const sequelize = require('./config/sequelize');
const associate = require('./relationships');
const DateUtilities = require('../utils/dateUtilities');
// Connect to the MongoDB database and log a message to the console
const connectMongoDb = async () => {
try {
if (process.env.APP_ENV === 'production') {
await mongoose.connect(process.env.MONGODB_URI);
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('MongoDB connection has been established successfully 🟢'));
} else {
mongoose.set('strictQuery', true);
await mongoose.connect(process.env.MONGODB_URI);
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('MongoDB connection has been established successfully 🟢'));
mongoose.set('debug', process.env.DEBUG);
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.red('debug :'), chalk.yellow(process.env.DEBUG));
}
} catch (error) {
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Unable to connect to the MongoDB database 🔴'));
console.error('MongoDB connection error:', error);
// Exit the process if the connection is not successful
process.exit(1);
}
};
// Connect to the MySQL database and log a message to the console
const connectMysqlDb = async () => {
try {
await sequelize.authenticate();
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('MySQL connection has been established successfully 🟢'));
// Check that required tables exist instead of synchronizing (no auto-create)
const queryInterface = sequelize.getQueryInterface();
let existingTables = [];
try {
existingTables = await queryInterface.showAllTables();
} catch (err) {
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Unable to list tables from MySQL 🔴'));
console.error(err);
// Exit immediately if unable to list tables
process.exit(1);
}
// Build expected table list from models exported in DB
const expectedTables = [];
Object.keys(DB).forEach((key) => {
const model = DB[key];
if (model && typeof model.getTableName === 'function') {
try {
const t = model.getTableName();
const tableName = typeof t === 'string' ? t : t.tableName;
if (tableName) expectedTables.push(tableName);
// eslint-disable-next-line no-unused-vars
} catch (err) {
// ignore
}
}
});
// Normalize table names to strings for comparison
const existingNormalized = existingTables.map(t => (typeof t === 'string' ? t : t.tableName)).map(t => t.toString());
const missing = expectedTables.filter(t => !existingNormalized.includes(t) && !existingNormalized.includes(t.toLowerCase()));
if (missing.length > 0) {
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Database schema validation failed - missing tables:'), chalk.yellow(missing.join(', ')));
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Run migrations to create the schema: npx sequelize-cli db:migrate'));
// Exit immediately if schema is incomplete
process.exit(1);
}
// If all expected tables are present, create associations
associate();
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('Database schema validated successfully 🟢'));
} catch (error) {
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Unable to connect to the MySQL database 🔴'));
console.error('MySQL connection error:', error);
// Exit the process if the connection is not successful
process.exit(1);
}
};
// Connect to all database and log a message to the console
const connectAllDb = async () => {
try {
await connectMongoDb();
await connectMysqlDb();
} catch (error) {
console.error('Failed to connect to databases:', error);
process.exit(1);
}
};
module.exports.connectAllDb = connectAllDb;
module.exports.connectMongoDb = connectMongoDb;
module.exports.connectMysqlDb = connectMysqlDb;
module.exports.default = connectAllDb;
@@ -1,624 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// Create User table
await queryInterface.createTable('user', {
id: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true
},
email: {
type: Sequelize.STRING(255),
allowNull: false,
unique: true
},
username: {
type: Sequelize.STRING(255),
allowNull: false,
unique: true
},
firstname: {
type: Sequelize.STRING(255),
allowNull: true
},
lastname: {
type: Sequelize.STRING(255),
allowNull: true
},
phone: {
type: Sequelize.STRING(20),
allowNull: true
},
image: {
type: Sequelize.STRING(128),
allowNull: true
},
role: {
type: Sequelize.STRING(20),
allowNull: true
},
salt: {
type: Sequelize.STRING(255),
allowNull: false
},
hash: {
type: Sequelize.TEXT,
allowNull: false
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Tag table
await queryInterface.createTable('tag', {
name: {
type: Sequelize.STRING(255),
allowNull: false,
primaryKey: true
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Role table
await queryInterface.createTable('role', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.STRING(255),
allowNull: true
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Brand table
await queryInterface.createTable('brand', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.STRING(255),
allowNull: true
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Category table
await queryInterface.createTable('category', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.STRING(255),
allowNull: true
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Packaging table
await queryInterface.createTable('packaging', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
name: {
type: Sequelize.STRING(255),
allowNull: true
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Article table
await queryInterface.createTable('article', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
slug: {
type: Sequelize.STRING(255),
allowNull: false
},
title: {
type: Sequelize.STRING(255),
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
body: {
type: Sequelize.TEXT,
allowNull: false
},
authorId: {
type: Sequelize.STRING(36),
allowNull: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Comment table
await queryInterface.createTable('comment', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
body: {
type: Sequelize.TEXT,
allowNull: false
},
authorId: {
type: Sequelize.STRING(36),
allowNull: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
articleId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'article',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create TagList table (junction table for Article-Tag)
await queryInterface.createTable('tagList', {
articleId: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'article',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
tagName: {
type: Sequelize.STRING(255),
allowNull: false,
primaryKey: true,
references: {
model: 'tag',
key: 'name'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Favorite table (junction table for User-Article)
await queryInterface.createTable('favorite', {
userId: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
articleId: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'article',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Follower table (junction table for User-User)
await queryInterface.createTable('follower', {
userId: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
followerId: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create Product table
await queryInterface.createTable('product', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
eancode: {
type: Sequelize.STRING(13),
allowNull: false,
unique: true
},
name: {
type: Sequelize.STRING(255),
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
stock: {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 0
},
weightGross: {
type: Sequelize.DECIMAL(10, 2),
allowNull: true,
defaultValue: 0.00
},
weightNet: {
type: Sequelize.DECIMAL(10, 2),
allowNull: true,
defaultValue: 0.00
},
priceBuy: {
type: Sequelize.DECIMAL(10, 2),
allowNull: true,
defaultValue: 0.00
},
priceRetail: {
type: Sequelize.DECIMAL(10, 2),
allowNull: true,
defaultValue: 0.00
},
priceTaxe: {
type: Sequelize.DECIMAL(10, 2),
allowNull: true,
defaultValue: 0.00
},
brandId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'brand',
key: 'id'
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE'
},
ownerId: {
type: Sequelize.STRING(36),
allowNull: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE'
},
packagingId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'packaging',
key: 'id'
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create UserRoleXref table (junction table for User-Role)
await queryInterface.createTable('userRoleXref', {
userId: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
roleId: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'role',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create ProductCategoryXref table
await queryInterface.createTable('productCategoryXref', {
productId: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'product',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
categoryId: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'category',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create ProductTagXref table
await queryInterface.createTable('productTagXref', {
productId: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
references: {
model: 'product',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
tagName: {
type: Sequelize.STRING(255),
allowNull: false,
primaryKey: true,
references: {
model: 'tag',
key: 'name'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
}, { transaction });
// Create indexes
await queryInterface.addIndex('article', ['authorId'], { transaction });
await queryInterface.addIndex('article', ['slug'], { transaction });
await queryInterface.addIndex('comment', ['authorId'], { transaction });
await queryInterface.addIndex('comment', ['articleId'], { transaction });
await queryInterface.addIndex('product', ['eancode'], { transaction });
await queryInterface.addIndex('product', ['brandId'], { transaction });
await queryInterface.addIndex('product', ['ownerId'], { transaction });
await queryInterface.addIndex('product', ['packagingId'], { transaction });
await queryInterface.addIndex('follower', ['followerId'], { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// Drop tables in reverse order of creation (due to foreign key constraints)
await queryInterface.dropTable('productTagXref', { transaction });
await queryInterface.dropTable('productCategoryXref', { transaction });
await queryInterface.dropTable('userRoleXref', { transaction });
await queryInterface.dropTable('product', { transaction });
await queryInterface.dropTable('follower', { transaction });
await queryInterface.dropTable('favorite', { transaction });
await queryInterface.dropTable('tagList', { transaction });
await queryInterface.dropTable('comment', { transaction });
await queryInterface.dropTable('article', { transaction });
await queryInterface.dropTable('packaging', { transaction });
await queryInterface.dropTable('category', { transaction });
await queryInterface.dropTable('brand', { transaction });
await queryInterface.dropTable('role', { transaction });
await queryInterface.dropTable('tag', { transaction });
await queryInterface.dropTable('user', { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
}
};
@@ -1,52 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.createTable('skydiver_profile', {
userId: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
licence: {
type: Sequelize.INTEGER,
allowNull: true
},
poids: {
type: Sequelize.FLOAT,
allowNull: true
},
bg_image: {
type: Sequelize.STRING(255),
allowNull: true
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
}
}, { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
async down(queryInterface) {
await queryInterface.dropTable('skydiver_profile');
}
};
@@ -1,63 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.createTable('application', {
id: {
type: Sequelize.STRING(36),
allowNull: false,
primaryKey: true
},
apikey: {
type: Sequelize.STRING(255),
allowNull: false
},
maskedkey: {
type: Sequelize.STRING(20),
allowNull: true
},
slug: {
type: Sequelize.STRING(255),
allowNull: false,
unique: true
},
title: {
type: Sequelize.STRING(255),
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
authorId: {
type: Sequelize.STRING(36),
allowNull: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'SET NULL'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false
}
}, { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
async down(queryInterface) {
await queryInterface.dropTable('application');
}
};
-35
View File
@@ -1,35 +0,0 @@
var mongoose = require('mongoose');
var slug = require('slug');
var AeronefSchema = new mongoose.Schema({
slug: { type: String, lowercase: true, unique: true },
aeronef: String,
imat: String,
author: { type: String }
}, {timestamps: true, toJSON: {virtuals: true}});
AeronefSchema.pre('validate', function (next) {
if (!this.slug) {
this.slugify();
}
next();
});
AeronefSchema.methods.slugify = function () {
this.slug = slug(`${this.aeronef} ${this.imat}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};
AeronefSchema.methods.toJSONFor = function(user){
return {
slug: this.slug,
aeronef: this.aeronef,
imat: this.imat,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author && typeof this.author.toProfileJSONFor === 'function'
? this.author.toProfileJSONFor(user)
: (user && typeof user.toProfileJSONFor === 'function' ? user.toProfileJSONFor() : this.author ?? null)
};
};
mongoose.model('Aeronef', AeronefSchema);
-35
View File
@@ -1,35 +0,0 @@
var mongoose = require('mongoose');
var slug = require('slug');
var CanopySchema = new mongoose.Schema({
slug: { type: String, lowercase: true, unique: true },
voile: String,
taille: Number,
author: { type: String }
}, {timestamps: true, toJSON: {virtuals: true}});
CanopySchema.pre('validate', function (next) {
if (!this.slug) {
this.slugify();
}
next();
});
CanopySchema.methods.slugify = function () {
this.slug = slug(`${this.voile} ${this.taille}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};
CanopySchema.methods.toJSONFor = function(user){
return {
slug: this.slug,
voile: this.voile,
taille: this.taille,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author && typeof this.author.toProfileJSONFor === 'function'
? this.author.toProfileJSONFor(user)
: (user && typeof user.toProfileJSONFor === 'function' ? user.toProfileJSONFor() : this.author ?? null)
};
};
mongoose.model('Canopy', CanopySchema);
-35
View File
@@ -1,35 +0,0 @@
var mongoose = require('mongoose');
var slug = require('slug');
var DropzoneSchema = new mongoose.Schema({
slug: { type: String, lowercase: true, unique: true },
lieu: String,
oaci: String,
author: { type: String }
}, {timestamps: true, toJSON: {virtuals: true}});
DropzoneSchema.pre('validate', function (next) {
if (!this.slug) {
this.slugify();
}
next();
});
DropzoneSchema.methods.slugify = function () {
this.slug = slug(`${this.lieu} ${this.oaci}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};
DropzoneSchema.methods.toJSONFor = function(user){
return {
slug: this.slug,
lieu: this.lieu,
oaci: this.oaci,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author && typeof this.author.toProfileJSONFor === 'function'
? this.author.toProfileJSONFor(user)
: (user && typeof user.toProfileJSONFor === 'function' ? user.toProfileJSONFor() : this.author ?? null)
};
};
mongoose.model('Dropzone', DropzoneSchema);
-88
View File
@@ -1,88 +0,0 @@
var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
var slug = require('slug');
var JumpSchema = new mongoose.Schema({
slug: { type: String, lowercase: true, unique: true },
date: { type: Date, default: Date.now },
numero: { type: Number, unique: true },
lieu: String,
oaci: String,
aeronef: String,
imat: String,
hauteur: Number,
deploiement: Number,
voile: String,
taille: Number,
categorie: String,
module: String,
participants: Number,
sautants: [String],
programme: String,
accessoires: String,
zone: String,
dossier: String,
video: String,
files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'JumpFile' }],
x2data: { type: mongoose.Schema.Types.ObjectId, ref: 'X2DataLog' },
author: { type: String }
}, {timestamps: true, toJSON: {virtuals: true}});
JumpSchema.plugin(uniqueValidator, { message: 'is already taken' });
JumpSchema.pre('validate', function (next) {
if (!this.slug) {
this.slugify();
}
next();
});
JumpSchema.methods.slugify = function () {
this.slug = slug(`${this.numero} ${this.lieu}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};
JumpSchema.methods.addFile = function (id) {
if (this.files.indexOf(id) === -1) {
this.files.push(id);
}
return this.save();
};
JumpSchema.methods.removeFile = function (id) {
this.files.remove(id);
return this.save();
};
JumpSchema.methods.toJSONFor = function(user) {
return {
slug: this.slug,
date: this.date,
numero: this.numero,
lieu: this.lieu,
oaci: this.oaci,
aeronef: this.aeronef,
imat: this.imat,
hauteur: this.hauteur,
deploiement: this.deploiement,
voile: this.voile,
taille: this.taille,
categorie: this.categorie,
module: this.module,
participants: this.participants,
sautants: this.sautants,
programme: this.programme,
accessoires: this.accessoires,
zone: this.zone,
dossier: this.dossier,
video: this.video,
files: this.files.map(file => file ? file.toJSONForJump() : null),
x2data: this.x2data ? this.x2data.toJSONFor() : null,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author && typeof this.author.toProfileJSONFor === 'function'
? this.author.toProfileJSONFor(user)
: (user && typeof user.toProfileJSONFor === 'function' ? user.toProfileJSONFor() : this.author ?? null)
};
};
mongoose.model('Jump', JumpSchema);
-53
View File
@@ -1,53 +0,0 @@
var mongoose = require('mongoose');
var uniqueValidator = require('mongoose-unique-validator');
var slug = require('slug');
var JumpFileSchema = new mongoose.Schema({
slug: { type: String, lowercase: true, unique: true },
name: String,
path: String,
type: {
type: String,
enum : ['csv', 'image', 'kml', 'video'],
default: 'video'
},
author: { type: String }
}, {timestamps: true, toJSON: {virtuals: true}});
JumpFileSchema.plugin(uniqueValidator, { message: 'is already taken' });
JumpFileSchema.pre('validate', function (next) {
if (!this.slug) {
this.slugify();
}
next();
});
JumpFileSchema.methods.slugify = function () {
this.slug = slug(`${this.type}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36) + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};
JumpFileSchema.methods.toJSONFor = function(user){
return {
slug: this.slug,
name: this.name,
path: this.path,
type: this.type,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
author: this.author && typeof this.author.toProfileJSONFor === 'function'
? this.author.toProfileJSONFor(user)
: (user && typeof user.toProfileJSONFor === 'function' ? user.toProfileJSONFor() : this.author ?? null)
};
};
JumpFileSchema.methods.toJSONForJump = function(){
return {
slug: this.slug,
name: this.name,
path: this.path,
type: this.type
};
};
mongoose.model('JumpFile', JumpFileSchema);

Some files were not shown because too many files have changed in this diff Show More