Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8edd76454 | |||
| 9bce9b714e | |||
| e741081966 | |||
| 2f78d85a27 | |||
| b68dd4c800 | |||
| 61fdac2434 | |||
| e83b2e831f | |||
| dd5dbbb0fe | |||
| 16b2de084b | |||
| d477cf239c | |||
| 9a534d5a17 |
@@ -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"
|
||||||
@@ -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
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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.)
|
|
||||||
@@ -59,15 +59,6 @@ Thumbs.db
|
|||||||
.scannerwork/
|
.scannerwork/
|
||||||
sonar.properties
|
sonar.properties
|
||||||
|
|
||||||
# TemporaryBackup files
|
|
||||||
**/*.copy.ts
|
**/*.copy.ts
|
||||||
**/*.copy.scss
|
**/*.copy.scss
|
||||||
**/*.copy.html
|
**/*.copy.html
|
||||||
|
|
||||||
# SQL Queries, Dumps and Notes
|
|
||||||
sql_queries/
|
|
||||||
sql_dumps/
|
|
||||||
sql_notes/
|
|
||||||
|
|
||||||
# Sequelize Auto Config
|
|
||||||
sequelize-auto-config.json
|
|
||||||
@@ -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'),
|
|
||||||
};
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
API pour l'application Ad Astra
|
||||||
|
|
||||||
## 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 |
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
Vendored
-40
@@ -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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Environment files - keep .example files only
|
|
||||||
.env*
|
|
||||||
|
|
||||||
# Keep example files for documentation
|
|
||||||
!.env.example
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 });
|
||||||
|
});
|
||||||
@@ -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 });
|
||||||
|
};
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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`).
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
═════════════════════════════════════════════════════════════════════════════
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const asyncHandler = (fn) => (req, res, next) => {
|
||||||
|
Promise.resolve(fn(req, res, next)).catch((err) => {
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = asyncHandler;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
exports.User = require('./User');
|
||||||
|
exports.Article = require('./Article');
|
||||||
|
exports.Comment = require('./Comment');
|
||||||
|
exports.Tag = require('./Tag');
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
@@ -1,5 +1,21 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env" : {
|
||||||
"APP_ENV": "local"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3485
-11037
File diff suppressed because it is too large
Load Diff
+38
-67
@@ -1,69 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "adastra-api",
|
"name": "adastra-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Ad Astra API",
|
"description": "Ad Astra API",
|
||||||
"main": "app.js",
|
"private": true,
|
||||||
"author": {
|
"license": "UNLICENSED",
|
||||||
"name": "Julien Gautier",
|
"author": "Solide Apps <jgautier.webdev@gmail.com>",
|
||||||
"email": "jgautier.webdev@gmail.com"
|
"contributors": [
|
||||||
},
|
"Julien Gautier <jgautier.webdev@gmail.com>"
|
||||||
"scripts": {
|
],
|
||||||
"start": "APP_ENV=production node ./app.js",
|
"main": "server.js",
|
||||||
"dev": "APP_ENV=development node ./app.js",
|
"scripts": {
|
||||||
"local": "APP_ENV=local nodemon ./app.js",
|
"start": "APP_ENV=production node ./server.js",
|
||||||
"test": "jest",
|
"dev": "APP_ENV=development node ./server.js",
|
||||||
"test:postman": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json",
|
"local": "APP_ENV=local nodemon ./server.js",
|
||||||
"stop": "lsof -ti :3200 | xargs kill",
|
"test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json"
|
||||||
"mongo:start": "docker run --name hu-mongo -p 27017:27017 mongo & sleep 5",
|
},
|
||||||
"mongo:stop": "docker stop hu-mongo && docker rm hu-mongo",
|
"engines": {
|
||||||
"create:user": "node scripts/create-user.js",
|
"node": "^16.20.2 || ^18.19.1 || ^20.11.1"
|
||||||
"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"
|
},
|
||||||
},
|
"dependencies": {
|
||||||
"engines": {
|
"@sendgrid/mail": "^8.1.5",
|
||||||
"node": "^16.20.2 || ^18.19.1 || ^20.11.1"
|
"bcryptjs": "^2.4.3",
|
||||||
},
|
"colors": "^1.4.0",
|
||||||
"license": "UNLICENSED",
|
"dotenv": "^16.0.3",
|
||||||
"private": true,
|
"express": "^4.18.2",
|
||||||
"dependencies": {
|
"jsonwebtoken": "^9.0.2",
|
||||||
"@sendgrid/mail": "^7.7.0",
|
"morgan": "^1.10.0",
|
||||||
"bcrypt": "^5.1.1",
|
"mysql2": "^3.14.3",
|
||||||
"cors": "^2.8.6",
|
"nodemon": "^3.1.10",
|
||||||
"dotenv": "^16.6.1",
|
"sequelize": "^6.26.0",
|
||||||
"ejs": "^3.1.10",
|
"slugify": "^1.6.5"
|
||||||
"express": "^5",
|
},
|
||||||
"express-jwt": "^8.5.1",
|
"devDependencies": {
|
||||||
"express-rate-limit": "^8.4.1",
|
"@eslint/js": "^9.0.0",
|
||||||
"express-session": "^1.19.0",
|
"eslint": "^9.0.0",
|
||||||
"express-validator": "^7.3.2",
|
"prettier": "^2.8.0",
|
||||||
"helmet": "^8.1.0",
|
"sequelize-cli": "^6.6.3"
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
var router = require('express').Router();
|
||||||
|
router.use('/api', require('./api'));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,26 +1,23 @@
|
|||||||
class DateUtilities {
|
const utils = {
|
||||||
static getDateString() {
|
getDateString: () => {
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let dateNow = today.toLocaleDateString();
|
let dateNow = today.toLocaleDateString();
|
||||||
return `${dateNow}`;
|
return `${dateNow}`;
|
||||||
}
|
},
|
||||||
|
getTimeString: () => {
|
||||||
static getTimeString() {
|
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let timeNow = today.toLocaleTimeString();
|
let timeNow = today.toLocaleTimeString();
|
||||||
return `${timeNow}`;
|
return `${timeNow}`;
|
||||||
}
|
},
|
||||||
|
getDateTimeString: (delimiter = true) => {
|
||||||
static getDateTimeString(delimiter = true) {
|
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let dateString = `${today.toLocaleDateString()} ${today.toLocaleTimeString()}`;
|
let dateString = `${today.toLocaleDateString()} ${today.toLocaleTimeString()}`;
|
||||||
if (delimiter) {
|
if (delimiter) {
|
||||||
dateString = `[${dateString}]`;
|
dateString = `[${dateString}]`;
|
||||||
}
|
}
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
},
|
||||||
|
getDateTimeISOString: (delimiter = true) => {
|
||||||
static getDateTimeISOString(delimiter = true) {
|
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let dateString = today.toISOString();
|
let dateString = today.toISOString();
|
||||||
if (delimiter) {
|
if (delimiter) {
|
||||||
@@ -28,6 +25,6 @@ class DateUtilities {
|
|||||||
}
|
}
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = DateUtilities;
|
module.exports = utils;
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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);
|
|
||||||
}));
|
|
||||||
@@ -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;
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 });
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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
Reference in New Issue
Block a user