From 6c1a8f7a7bf71f7081d0b957d2f1f68040eba825 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Fri, 26 Sep 2025 09:18:48 +0200 Subject: [PATCH 01/55] chore(env): add .env.example and install dotenv for environment variables management --- .env.example | 9 +++++++++ package-lock.json | 13 +++++++++++++ package.json | 1 + 3 files changed, 23 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e42b39e --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +DB_USERNAME=tu_usuario +DB_PASSWORD=tu_contraseña +DB_DEV_NAME= +DB_TEST_NAME= +DB_HOST= +DB_PORT= +DB_DIALECT= +NODE_ENV= +PORT= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a60e0c5..38bc1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "bcryptjs": "^3.0.2", + "dotenv": "^17.2.2", "express": "^5.1.0" }, "devDependencies": { @@ -2369,6 +2370,18 @@ "wrappy": "1" } }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index cbce05b..f94986c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "description": "", "dependencies": { "bcryptjs": "^3.0.2", + "dotenv": "^17.2.2", "express": "^5.1.0" }, "devDependencies": { From 29a2d7d0c6620598cf793e03ff6482c7e4ad2674 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Fri, 26 Sep 2025 11:52:31 +0200 Subject: [PATCH 02/55] chore(env): update .env.example with new environment variables --- .env.example | 11 +- database/{.gitkeep => db_connection.ts} | 0 models/{.gitkeep => ArticleModel.ts} | 0 models/UserModel.ts | 0 package-lock.json | 196 +++++++++++++++++++++++- package.json | 3 +- 6 files changed, 199 insertions(+), 11 deletions(-) rename database/{.gitkeep => db_connection.ts} (100%) rename models/{.gitkeep => ArticleModel.ts} (100%) create mode 100644 models/UserModel.ts diff --git a/.env.example b/.env.example index e42b39e..78d0283 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,6 @@ -DB_USERNAME=tu_usuario -DB_PASSWORD=tu_contraseña -DB_DEV_NAME= -DB_TEST_NAME= +DB_NAME= +DB_USER= +DB_PASS= DB_HOST= -DB_PORT= -DB_DIALECT= -NODE_ENV= +DIALECT= PORT= \ No newline at end of file diff --git a/database/.gitkeep b/database/db_connection.ts similarity index 100% rename from database/.gitkeep rename to database/db_connection.ts diff --git a/models/.gitkeep b/models/ArticleModel.ts similarity index 100% rename from models/.gitkeep rename to models/ArticleModel.ts diff --git a/models/UserModel.ts b/models/UserModel.ts new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 38bc1f5..3f1c4ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", - "express": "^5.1.0" + "express": "^5.1.0", + "sequelize": "^6.37.7" }, "devDependencies": { "@types/express": "^5.0.3", @@ -1160,6 +1161,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/express": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", @@ -1244,11 +1254,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.12.0" @@ -1322,6 +1337,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2382,6 +2403,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3119,6 +3146,15 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3999,6 +4035,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4203,6 +4245,27 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4488,6 +4551,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4684,6 +4753,12 @@ "node": ">=8" } }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -4758,6 +4833,89 @@ "node": ">= 18" } }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -5275,6 +5433,12 @@ "node": ">=0.6" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, "node_modules/ts-jest": { "version": "29.4.4", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", @@ -5432,7 +5596,6 @@ "version": "7.12.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -5510,6 +5673,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -5525,6 +5697,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5560,6 +5741,15 @@ "node": ">= 8" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index f94986c..6588f72 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "dependencies": { "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", - "express": "^5.1.0" + "express": "^5.1.0", + "sequelize": "^6.37.7" }, "devDependencies": { "@types/express": "^5.0.3", From 949f1a99d13a44a9f5781cefdc8dc07e40c98817 Mon Sep 17 00:00:00 2001 From: olgararo Date: Fri, 26 Sep 2025 12:15:05 +0200 Subject: [PATCH 03/55] chore(config): add tsconfig.json with strict TypeScript settings for backend --- models/{ArticleModel.ts => ArticleModel.js} | 0 models/{UserModel.ts => UserModel.js} | 0 package.json | 1 + tsconfig.json | 25 +++++++++++++++++++++ 4 files changed, 26 insertions(+) rename models/{ArticleModel.ts => ArticleModel.js} (100%) rename models/{UserModel.ts => UserModel.js} (100%) create mode 100644 tsconfig.json diff --git a/models/ArticleModel.ts b/models/ArticleModel.js similarity index 100% rename from models/ArticleModel.ts rename to models/ArticleModel.js diff --git a/models/UserModel.ts b/models/UserModel.js similarity index 100% rename from models/UserModel.ts rename to models/UserModel.js diff --git a/package.json b/package.json index 6588f72..302fcae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "codigo-abisal-server", "version": "1.0.0", + "type": "module", "main": "app.ts", "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watchAll --no-cache" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6624091 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + /* --- Direcciones de los ficheros --- */ + "rootDir": "./src", // El código fuente TypeScript está aquí. + "outDir": "./dist", // El código JavaScript compilado irá aquí. + + /* --- Entorno y Módulos --- */ + "module": "nodenext", // Usa el sistema de módulos moderno de Node. + "target": "esnext", // Compila a la última versión de JS. + "lib": ["esnext"], // Librerías base para un entorno moderno. + "esModuleInterop": true, // Mejora la compatibilidad entre módulos. + "skipLibCheck": true, // Evita revisar los tipos de las librerías de terceros. + + /* --- Calidad y Reglas Estrictas (¡muy recomendado!) --- */ + "strict": true, // Activa todas las reglas estrictas. + "forceConsistentCasingInFileNames": true, // Evita errores de mayúsculas/minúsculas en los nombres de archivo. + "noUnusedLocals": true, // Avisa si hay variables locales sin usar. + "noUnusedParameters": true, // Avisa si hay parámetros de función sin usar. + + /* --- Salida de la compilación --- */ + "sourceMap": true // Genera "mapas" para facilitar el debug. + }, + "include": ["src/**/*"], // Asegúrate de compilar todo lo que esté dentro de la carpeta src. + "exclude": ["node_modules"] // Ignora la carpeta node_modules. +} From ebfab8a7a52b743a1e15383ccddd0bd13e8af49b Mon Sep 17 00:00:00 2001 From: olgararo Date: Fri, 26 Sep 2025 12:19:08 +0200 Subject: [PATCH 04/55] chore(config): add src folder --- src/.gitkeep | 0 src/prueba.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/.gitkeep create mode 100644 src/prueba.ts diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/prueba.ts b/src/prueba.ts new file mode 100644 index 0000000..e69de29 From 5f6ef3b700ea347e4d82b407c15db3edfa7f00f5 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 29 Sep 2025 09:03:55 +0200 Subject: [PATCH 05/55] refactor(project): move backend folders into src and setup MySQL database with Sequelize sync --- app.ts | 0 database/db_connection.ts | 0 models/ArticleModel.js | 0 models/UserModel.js | 0 package-lock.json | 659 +++++++++++++++++++++- package.json | 13 +- src/app.ts | 30 + {controllers => src/controllers}/.gitkeep | 0 src/database/db_connection.ts | 18 + src/models/ArticleModel.ts | 144 +++++ src/models/UserModel.ts | 110 ++++ src/prueba.ts | 0 {routes => src/routes}/.gitkeep | 0 src/{ => validators}/.gitkeep | 0 tsconfig.json | 65 ++- validators/.gitkeep | 0 16 files changed, 1016 insertions(+), 23 deletions(-) delete mode 100644 app.ts delete mode 100644 database/db_connection.ts delete mode 100644 models/ArticleModel.js delete mode 100644 models/UserModel.js create mode 100644 src/app.ts rename {controllers => src/controllers}/.gitkeep (100%) create mode 100644 src/database/db_connection.ts create mode 100644 src/models/ArticleModel.ts create mode 100644 src/models/UserModel.ts delete mode 100644 src/prueba.ts rename {routes => src/routes}/.gitkeep (100%) rename src/{ => validators}/.gitkeep (100%) delete mode 100644 validators/.gitkeep diff --git a/app.ts b/app.ts deleted file mode 100644 index e69de29..0000000 diff --git a/database/db_connection.ts b/database/db_connection.ts deleted file mode 100644 index e69de29..0000000 diff --git a/models/ArticleModel.js b/models/ArticleModel.js deleted file mode 100644 index e69de29..0000000 diff --git a/models/UserModel.js b/models/UserModel.js deleted file mode 100644 index e69de29..0000000 diff --git a/package-lock.json b/package-lock.json index 3f1c4ff..a486c58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,19 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "mysql2": "^3.15.1", "sequelize": "^6.37.7" }, "devDependencies": { "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", "jest": "^30.1.3", "supertest": "^7.1.4", - "ts-jest": "^29.4.4" + "ts-jest": "^29.4.4", + "tsx": "^4.20.6", + "typescript": "^5.9.2" } }, "node_modules/@babel/code-frame": { @@ -553,6 +557,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1732,6 +2178,15 @@ "dev": true, "license": "MIT" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/babel-jest": { "version": "30.1.2", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", @@ -2361,6 +2816,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2528,6 +2992,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2870,6 +3376,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2950,6 +3465,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3225,6 +3753,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4048,6 +4582,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4058,6 +4598,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -4272,6 +4827,63 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.1.tgz", + "integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/napi-postinstall": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", @@ -4753,6 +5365,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/retry-as-promised": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", @@ -4833,6 +5455,11 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/sequelize": { "version": "6.37.7", "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", @@ -5083,6 +5710,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -5526,6 +6162,26 @@ "license": "0BSD", "optional": true }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5569,7 +6225,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 302fcae..1d06ea1 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,12 @@ "name": "codigo-abisal-server", "version": "1.0.0", "type": "module", - "main": "app.ts", + "main": "dist/app.js", "scripts": { - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watchAll --no-cache" + "dev": "tsx src/app.ts", + "build": "tsc", + "start": "node dist/app.js", + "test": "jest --watchAll --no-cache" }, "repository": { "type": "git", @@ -22,14 +25,18 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "mysql2": "^3.15.1", "sequelize": "^6.37.7" }, "devDependencies": { "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", "jest": "^30.1.3", "supertest": "^7.1.4", - "ts-jest": "^29.4.4" + "ts-jest": "^29.4.4", + "tsx": "^4.20.6", + "typescript": "^5.9.2" } } diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..1e429ec --- /dev/null +++ b/src/app.ts @@ -0,0 +1,30 @@ +import express from "express"; +import db_connection from "../src/database/db_connection.js"; +import "dotenv/config"; +import "./models/UserModel"; +import "./models/ArticleModel"; + + + export const app = express(); + const PORT = process.env.PORT || 8080; + + app.use(express.json()); + app.get("/", (_req, res) => { + res.send("Hola API"); +}); + +async function startServer() { + try { + // Sincroniza los modelos con la base de datos + await db_connection.sync(); // OJO: Ver las opciones más abajo + console.log("✅ Database synchronized successfully."); + + app.listen(PORT, () => { + console.log(`🚀 Server is running on port ${PORT}`); + }); + } catch (error) { + console.error("❌ Unable to sync database:", error); + } +} + +startServer(); \ No newline at end of file diff --git a/controllers/.gitkeep b/src/controllers/.gitkeep similarity index 100% rename from controllers/.gitkeep rename to src/controllers/.gitkeep diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts new file mode 100644 index 0000000..c652ddc --- /dev/null +++ b/src/database/db_connection.ts @@ -0,0 +1,18 @@ +import { Sequelize } from "sequelize"; +import dotenv from "dotenv"; +dotenv.config(); + +const db_connection = new Sequelize( + process.env.DB_NAME as string, + process.env.DB_USER as string, + process.env.DB_PASS as string, + { + host: "localhost", + dialect: "mysql", + define: { + timestamps: false, //esta parte es un añadido por lo de createAT y updateAt + }, + } +); + +export default db_connection; \ No newline at end of file diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts new file mode 100644 index 0000000..a4cbf03 --- /dev/null +++ b/src/models/ArticleModel.ts @@ -0,0 +1,144 @@ +// 1) Importo tipos y utilidades de Sequelize +import { DataTypes, Model, Optional } from "sequelize"; +// 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) +import db_connection from "../database/db_connection.js"; + +// 3) Declaro cómo es un usuario en la BD (TODOS los campos) +export interface ArticleAttributes { + id: bigint; + creator_id: bigint; //TENER EN CUENTA QUE DEBE VENIR DEL MODELO DE USER + title: string; + description: string; + content:string; + category: string; + species: string; + image?: string; + references?: string; +// likes_count: bigint; + created_at: Date; + updated_at: Date; +} + +// 4) Campos opcionales AL CREAR (Sequelize los rellena solo) +export type ArticleCreationAttributes = Optional< + ArticleAttributes, + "id" | "created_at" | "updated_at" | "image" | "references" +>; + +// 5) Defino la clase del modelo (tipada) +export class Article + extends Model + implements ArticleAttributes +{ + public id!: bigint; + public creator_id!: bigint; + public title!: string; + public description!: string; + public content!: string; + public category!: string; + public species!: string; + public image!: string; + public references!: string; + public created_at!: Date; + public updated_at!: Date; + +} + + +// 6) Inicializo (equivalente a define) y mapeo columnas/validaciones +Article.init( + { + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "titulo no puede estar vacío" }, + len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, + }, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "descripcion no puede estar vacío" }, + len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, + }, + }, + + content: { + type: DataTypes.TEXT("long"), + allowNull: false, + validate: { + notNull: { msg: "content no puede estar vacío" }, + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + category: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "content no puede estar vacío" }, + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + + species: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "content no puede estar vacío" }, + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + image: { + type: DataTypes.STRING, + allowNull: true, + validate: { + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + + references: { + type: DataTypes.STRING, + allowNull: true, + validate: { + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + creator_id: { + type: DataTypes.BIGINT, + allowNull: false, + references: { + model: 'users', + key: 'id' + }, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { + sequelize: db_connection, // ← tu conexión + tableName: "articles", // ← nombre real de la tabla + timestamps: true, // ← activa created/updated + underscored: true, // ← columnas snake_case + createdAt: "created_at", // ← mapea el nombre + updatedAt: "updated_at", // ← mapea el nombre + } +); + +// 7) ¡Exporto el modelo! (puedes default o nombrado) +export default Article; + diff --git a/src/models/UserModel.ts b/src/models/UserModel.ts new file mode 100644 index 0000000..a2d55d9 --- /dev/null +++ b/src/models/UserModel.ts @@ -0,0 +1,110 @@ +// 1) Importo tipos y utilidades de Sequelize +import { DataTypes, Model, Optional } from "sequelize"; +// 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) +import db_connection from "../database/db_connection.js" + +// 3) Declaro cómo es un usuario en la BD (TODOS los campos) +export interface UserAttributes { + id: bigint; + username: string; + email: string; + password: string; + name: string; + last_name: string; + role: string; + created_at: Date; + updated_at: Date; +} + +// 4) Campos opcionales AL CREAR (Sequelize los rellena solo) +export type UserCreationAttributes = Optional< + UserAttributes, + "id" | "created_at" | "updated_at" +>; + +// 5) Defino la clase del modelo (tipada) +export class User + extends Model + implements UserAttributes +{ + public id!: bigint; + public username!: string; + public email!: string; + public password!: string; + public name!: string; + public last_name!: string; + public role!: string; + public created_at!: Date; + public updated_at!: Date; +} + +// 6) Inicializo (equivalente a define) y mapeo columnas/validaciones +User.init( + { + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "username no puede estar vacío" }, + len: { args: [2, 255], msg: "username mínimo 2 caracteres" }, + }, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + notNull: { msg: "email no puede estar vacío" }, + isEmail: { msg: "email no es válido" }, + }, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "password no puede estar vacío" }, + len: { args: [6, 255], msg: "password mínimo 6 caracteres" }, + }, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + last_name: { + type: DataTypes.STRING, + allowNull: false, + }, + role: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "user", + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { + sequelize: db_connection, // ← tu conexión + tableName: "users", // ← nombre real de la tabla + timestamps: true, // ← activa created/updated + underscored: true, // ← columnas snake_case + createdAt: "created_at", // ← mapea el nombre + updatedAt: "updated_at", // ← mapea el nombre + } +); + +// 7) ¡Exporto el modelo! (puedes default o nombrado) +export default User; + diff --git a/src/prueba.ts b/src/prueba.ts deleted file mode 100644 index e69de29..0000000 diff --git a/routes/.gitkeep b/src/routes/.gitkeep similarity index 100% rename from routes/.gitkeep rename to src/routes/.gitkeep diff --git a/src/.gitkeep b/src/validators/.gitkeep similarity index 100% rename from src/.gitkeep rename to src/validators/.gitkeep diff --git a/tsconfig.json b/tsconfig.json index 6624091..3678a1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,54 @@ +// { +// "compilerOptions": { +// /* --- Direcciones de los ficheros --- */ +// "rootDir": "./src", // El código fuente TypeScript está aquí. +// "outDir": "./dist", // El código JavaScript compilado irá aquí. + +// /* --- Entorno y Módulos --- */ +// "module": "nodenext", // Usa el sistema de módulos moderno de Node. +// "target": "esnext", // Compila a la última versión de JS. +// "lib": ["esnext"], // Librerías base para un entorno moderno. +// "esModuleInterop": true, // Mejora la compatibilidad entre módulos. +// "skipLibCheck": true, // Evita revisar los tipos de las librerías de terceros. + +// /* --- Calidad y Reglas Estrictas (¡muy recomendado!) --- */ +// "strict": true, // Activa todas las reglas estrictas. +// "forceConsistentCasingInFileNames": true, // Evita errores de mayúsculas/minúsculas en los nombres de archivo. +// "noUnusedLocals": true, // Avisa si hay variables locales sin usar. +// "noUnusedParameters": true, // Avisa si hay parámetros de función sin usar. + +// /* --- Salida de la compilación --- */ +// "sourceMap": true // Genera "mapas" para facilitar el debug. +// }, +// "include": ["src/**/*"], // Asegúrate de compilar todo lo que esté dentro de la carpeta src. +// "exclude": ["node_modules"] // Ignora la carpeta node_modules. +// } + { "compilerOptions": { - /* --- Direcciones de los ficheros --- */ - "rootDir": "./src", // El código fuente TypeScript está aquí. - "outDir": "./dist", // El código JavaScript compilado irá aquí. + /* --- Rutas --- */ + "rootDir": "./src", // Tu código .ts vive en src + "outDir": "./dist", // El JS compilado saldrá en dist - /* --- Entorno y Módulos --- */ - "module": "nodenext", // Usa el sistema de módulos moderno de Node. - "target": "esnext", // Compila a la última versión de JS. - "lib": ["esnext"], // Librerías base para un entorno moderno. - "esModuleInterop": true, // Mejora la compatibilidad entre módulos. - "skipLibCheck": true, // Evita revisar los tipos de las librerías de terceros. + /* --- Módulos/Entorno (Node ESM) --- */ + "module": "nodenext", // ESM nativo de Node + "moduleResolution": "nodenext", // Resolver imports al estilo Node ESM + "target": "ES2022", // JS moderno compatible con Node 22 + "lib": ["ES2022"], // Librerías base + "types": ["node"], // Tipos de Node (fs, process, etc.) + "resolveJsonModule": true, // Permite import de .json (opcional) - /* --- Calidad y Reglas Estrictas (¡muy recomendado!) --- */ - "strict": true, // Activa todas las reglas estrictas. - "forceConsistentCasingInFileNames": true, // Evita errores de mayúsculas/minúsculas en los nombres de archivo. - "noUnusedLocals": true, // Avisa si hay variables locales sin usar. - "noUnusedParameters": true, // Avisa si hay parámetros de función sin usar. + /* --- Compatibilidad y calidad --- */ + "esModuleInterop": true, // Imports por defecto más cómodos + "skipLibCheck": true, // Acelera compilación + "strict": true, // Reglas estrictas + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, - /* --- Salida de la compilación --- */ - "sourceMap": true // Genera "mapas" para facilitar el debug. + /* --- Debug --- */ + "sourceMap": true }, - "include": ["src/**/*"], // Asegúrate de compilar todo lo que esté dentro de la carpeta src. - "exclude": ["node_modules"] // Ignora la carpeta node_modules. + "include": ["src/**/*"], + "exclude": ["node_modules"] } diff --git a/validators/.gitkeep b/validators/.gitkeep deleted file mode 100644 index e69de29..0000000 From 41a548013b3bbdce216f22c2432a51b602aed1aa Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 30 Sep 2025 11:07:12 +0200 Subject: [PATCH 06/55] feat(api): add Article and User Authentication controllers --- package-lock.json | 133 +++++++++++++++++++++++++++ package.json | 3 + src/controllers/.gitkeep | 0 src/controllers/ArticleController.ts | 104 +++++++++++++++++++++ src/controllers/AuthController.ts | 92 ++++++++++++++++++ 5 files changed, 332 insertions(+) delete mode 100644 src/controllers/.gitkeep create mode 100644 src/controllers/ArticleController.ts create mode 100644 src/controllers/AuthController.ts diff --git a/package-lock.json b/package-lock.json index a486c58..1a3714a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,15 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.15.1", "sequelize": "^6.37.7" }, "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", "jest": "^30.1.3", @@ -1579,6 +1582,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1686,6 +1696,17 @@ "pretty-format": "^30.0.0" } }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2414,6 +2435,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2894,6 +2921,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4539,6 +4575,61 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4575,6 +4666,42 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4582,6 +4709,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", diff --git a/package.json b/package.json index 1d06ea1..ab2901a 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,15 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.15.1", "sequelize": "^6.37.7" }, "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", "jest": "^30.1.3", diff --git a/src/controllers/.gitkeep b/src/controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts new file mode 100644 index 0000000..950184f --- /dev/null +++ b/src/controllers/ArticleController.ts @@ -0,0 +1,104 @@ +import { Article } from "../models/ArticleModel.js"; +import type { Request, Response } from "express"; + +export const getAllArticles = async (_req: Request, res: Response) => { + try { + const articles = await Article.findAll() + res.status(200).json(articles) + } catch (error) { + + res.status(500).json({ message: "Error obteniendo artículos", error }); + } +}; + +export const getArticleById = async (req: Request<{ id: string }>, res: Response) => { + try { + + const { id } = req.params; + + // Buscamos por clave primaria con Sequelize (findByPk) + const article = await Article.findByPk(id); + + // Si no existe, respondemos 404 Not Found + if (!article) { + return res.status(404).json({ message: "Artículo no encontrado" }); + } + // Si existe, 200 OK con el artículo + res.status(200).json(article); + } catch (error) { + // Cualquier error inesperado -> 500 + res.status(500).json({ message: "Error obteniendo el artículo" }); + } + +} + +export const deleteArticle = async (req: Request, res: Response) => { + try { + const deleted = await Article.destroy({ where: { id: req.params.id } }); + if (deleted === 0) { + return res.status(404).json({ message: "Artículo no encontrado" }); + } + res.status(200).json({ message: "El articulo esta eliminado correctamente" }); + } catch (error) { + res.status(500).json({ message: "No se pudo eliminar el articulo" }); + } +} + +export const createArticle = async (req: Request, res: Response) => { + try { + // Aquí filtramos los campos que sí queremos guardar + const {title, description, content, category, species, image, references, creator_id, } = req.body; + + // Creamos el artículo solo con esos campos (los demás se ignoran) + const newArticle = await Article.create({ + title, + description, + content, + category, + species, + image, + references, + creator_id, + }); + + return res.status(201).json(newArticle); + } catch (error) { + return res.status(500).json({ message: "No se pudo crear el artículo" }); + } +}; + +//Defino el tipo del body para el update (todos opcionales) +interface UpdateArticleDTO { + title?: string; + description?: string; + content?: string; + category?: string; + species?: string; + image?: string; + references?: string; +} +export const updateArticle = async ( + req: Request<{ id: string }, unknown, UpdateArticleDTO>, + res: Response +) => { + try { + const { id } = req.params; + const article = await Article.findByPk(id); + + if (!article) { + return res.status(404).json({ message: "Artículo no encontrado" }); + } + + // Filtramos los campos que pueden actualizarse + const { title, description, content, category, species, image, references } = req.body; + + await article.update({ title, description, content, category, species, image, references,}); + + return res.status(200).json({ + message: "Artículo actualizado correctamente", + article, + }); + } catch (_error) { + return res.status(500).json({ message: "Error actualizando el artículo" }); + } +}; diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts new file mode 100644 index 0000000..88999dd --- /dev/null +++ b/src/controllers/AuthController.ts @@ -0,0 +1,92 @@ +import { Request, Response } from "express"; +import UserModel from "../models/UserModel.js"; +import bcrypt from "bcryptjs"; + +interface RegisterBody { + name: string; + email: string; + password: string; +} + +interface LoginBody { + email: string; + password: string; +} + +// Registro de Usuarios +export const registerController = async ( + req: Request<{}, {}, RegisterBody>, + res: Response +): Promise => { + try { + const { name, email, password } = req.body; + + if (!name || !email || !password) { + res.status(400).json({ message: "name, email y password son requeridos" }); + return; + } + + const emailNorm = email.toLowerCase().trim(); + + // (opcional) verifica si ya existe + const exists = await UserModel.findOne({ where: { email: emailNorm } }); + if (exists) { + res.status(409).json({ message: "El email ya está registrado" }); + return; + } + + const hashPassword = await bcrypt.hash(password, 10); + + const newUser = await UserModel.create({ + name, + email: emailNorm, + // 👇 Usa la columna real en BD: + password_hash: hashPassword, + }); + + res.status(201).json({ + message: "Usuario registrado exitosamente", + userId: newUser.id, + }); + } catch (error) { + res.status(500).json({ message: error instanceof Error ? error.message : "Unexpected error" }); + } +}; + +// Login de Usuarios +export const loginController = async ( + req: Request<{}, {}, LoginBody>, + res: Response +): Promise => { + try { + const { email, password } = req.body; + + if (!email || !password) { + res.status(400).json({ message: "email y password son requeridos" }); + return; + } + + const emailNorm = email.toLowerCase().trim(); + + // Trae explícitamente el hash + const user = await UserModel.findOne({ + where: { email: emailNorm }, + attributes: ["id", "name", "email", "password_hash"], + }); + + if (!user) { + res.status(404).json({ message: "Usuario no encontrado" }); + return; + } + + const ok = await bcrypt.compare(password, user.password_hash); + if (!ok) { + res.status(401).json({ message: "Contraseña incorrecta" }); + return; + } + + res.status(200).json({ message: "Login exitoso" }); + } catch (error) { + res.status(500).json({ message: error instanceof Error ? error.message : "Unexpected error" }); + } +}; From 836ce78dfbaf267e6aeaf8709a5088e896dcfbfe Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 30 Sep 2025 11:21:58 +0200 Subject: [PATCH 07/55] feat(auth): add user registration and login with email normalization and password hashing --- src/controllers/AuthController.ts | 62 ++++++++++++++++++------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 88999dd..0198d39 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,11 +1,14 @@ import { Request, Response } from "express"; -import UserModel from "../models/UserModel.js"; import bcrypt from "bcryptjs"; +import UserModel from "../models/UserModel.js"; interface RegisterBody { + username: string; // Añadir name: string; + last_name: string; // Añadir email: string; password: string; + role: string; } interface LoginBody { @@ -13,35 +16,42 @@ interface LoginBody { password: string; } -// Registro de Usuarios +// 👉 Registro de usuarios export const registerController = async ( req: Request<{}, {}, RegisterBody>, res: Response ): Promise => { try { - const { name, email, password } = req.body; + // 1. Desestructura los nuevos campos + const { username, name, last_name, email, password, role} = req.body; - if (!name || !email || !password) { - res.status(400).json({ message: "name, email y password son requeridos" }); + // 2. Añádelos a la validación + if (!username || !name || !last_name || !email || !password) { + res.status(400).json({ message: "Todos los campos son requeridos" }); return; } - const emailNorm = email.toLowerCase().trim(); + // Normalizamos email + const normalizedEmail = email.toLowerCase().trim(); - // (opcional) verifica si ya existe - const exists = await UserModel.findOne({ where: { email: emailNorm } }); - if (exists) { + // verificar si el email ya existe + const existingUser = await UserModel.findOne({ where: { email: normalizedEmail } }); + if (existingUser) { res.status(409).json({ message: "El email ya está registrado" }); return; } + // hashear contraseña const hashPassword = await bcrypt.hash(password, 10); + // 3. Pasa todos los campos requeridos a .create() const newUser = await UserModel.create({ + username, name, - email: emailNorm, - // 👇 Usa la columna real en BD: - password_hash: hashPassword, + last_name, + email: normalizedEmail, + password: hashPassword, + role }); res.status(201).json({ @@ -49,11 +59,13 @@ export const registerController = async ( userId: newUser.id, }); } catch (error) { - res.status(500).json({ message: error instanceof Error ? error.message : "Unexpected error" }); + res.status(500).json({ + message: error instanceof Error ? error.message : "Unexpected error", + }); } }; -// Login de Usuarios +// 👉 Login de usuarios export const loginController = async ( req: Request<{}, {}, LoginBody>, res: Response @@ -62,24 +74,22 @@ export const loginController = async ( const { email, password } = req.body; if (!email || !password) { - res.status(400).json({ message: "email y password son requeridos" }); + res.status(400).json({ message: "Email y password son requeridos" }); return; } - const emailNorm = email.toLowerCase().trim(); - - // Trae explícitamente el hash - const user = await UserModel.findOne({ - where: { email: emailNorm }, - attributes: ["id", "name", "email", "password_hash"], - }); + // Normalizamos email + const normalizedEmail = email.toLowerCase().trim(); + // buscar usuario por email normalizado + const user = await UserModel.findOne({ where: { email: normalizedEmail } }); if (!user) { res.status(404).json({ message: "Usuario no encontrado" }); return; } - const ok = await bcrypt.compare(password, user.password_hash); + // comparar contraseña ingresada con la hasheada en BD + const ok = await bcrypt.compare(password, user.password); if (!ok) { res.status(401).json({ message: "Contraseña incorrecta" }); return; @@ -87,6 +97,8 @@ export const loginController = async ( res.status(200).json({ message: "Login exitoso" }); } catch (error) { - res.status(500).json({ message: error instanceof Error ? error.message : "Unexpected error" }); + res.status(500).json({ + message: error instanceof Error ? error.message : "Unexpected error", + }); } -}; +}; \ No newline at end of file From ca1193c3e5385702654e5f1f94161d2e3f49cf91 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Tue, 30 Sep 2025 11:32:17 +0200 Subject: [PATCH 08/55] feat: Add article routes --- src/routes/articleRoutes.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/routes/articleRoutes.ts diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts new file mode 100644 index 0000000..ce8099d --- /dev/null +++ b/src/routes/articleRoutes.ts @@ -0,0 +1,15 @@ +import express from 'express' +import {getAllArticles, getArticleById, deleteArticle, createArticle , updateArticle} from '../controllers/ArticleController.js' +const articleRouter = express.Router() + +articleRouter.get("/", getAllArticles) + +articleRouter.get("/:id", getArticleById) + +articleRouter.post("/", createArticle) + +articleRouter.delete("/:id", deleteArticle) + +articleRouter.put("/:id", updateArticle) + +export default articleRouter \ No newline at end of file From 435bb8fba5eaabfbe96c20713424d6384c1fe768 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Tue, 30 Sep 2025 11:34:00 +0200 Subject: [PATCH 09/55] feat: Add auth routes --- src/routes/.gitkeep | 0 src/routes/authRoutes.ts | 9 +++++++++ 2 files changed, 9 insertions(+) delete mode 100644 src/routes/.gitkeep create mode 100644 src/routes/authRoutes.ts diff --git a/src/routes/.gitkeep b/src/routes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/authRoutes.ts b/src/routes/authRoutes.ts new file mode 100644 index 0000000..4dda64d --- /dev/null +++ b/src/routes/authRoutes.ts @@ -0,0 +1,9 @@ +import express from 'express' +import { registerController, loginController } from '../controllers/AuthController.js' +const authRouter = express.Router() + + +authRouter.post("/register", registerController) +authRouter.post("/login", loginController) + +export default authRouter \ No newline at end of file From db0f3dfd0035a12485fb8bfb290f16a1b1d1ca96 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Tue, 30 Sep 2025 12:16:06 +0200 Subject: [PATCH 10/55] fix: use declare keyword in Sequelize models to prevent field shadowing Replace public class fields with declare keyword in User and Article models to avoid shadowing Sequelize's attribute getters and setters. --- dist/app.js | 30 ++++++ dist/app.js.map | 1 + dist/controllers/ArticleController.js | 80 ++++++++++++++++ dist/controllers/ArticleController.js.map | 1 + dist/controllers/AuthController.js | 73 +++++++++++++++ dist/controllers/AuthController.js.map | 1 + dist/database/db_connection.js | 12 +++ dist/database/db_connection.js.map | 1 + dist/models/ArticleModel.js | 108 ++++++++++++++++++++++ dist/models/ArticleModel.js.map | 1 + dist/models/UserModel.js | 82 ++++++++++++++++ dist/models/UserModel.js.map | 1 + dist/routes/articleRoutes.js | 10 ++ dist/routes/articleRoutes.js.map | 1 + dist/routes/authRoutes.js | 7 ++ dist/routes/authRoutes.js.map | 1 + src/app.ts | 5 +- src/models/ArticleModel.ts | 22 ++--- src/models/UserModel.ts | 18 ++-- 19 files changed, 434 insertions(+), 21 deletions(-) create mode 100644 dist/app.js create mode 100644 dist/app.js.map create mode 100644 dist/controllers/ArticleController.js create mode 100644 dist/controllers/ArticleController.js.map create mode 100644 dist/controllers/AuthController.js create mode 100644 dist/controllers/AuthController.js.map create mode 100644 dist/database/db_connection.js create mode 100644 dist/database/db_connection.js.map create mode 100644 dist/models/ArticleModel.js create mode 100644 dist/models/ArticleModel.js.map create mode 100644 dist/models/UserModel.js create mode 100644 dist/models/UserModel.js.map create mode 100644 dist/routes/articleRoutes.js create mode 100644 dist/routes/articleRoutes.js.map create mode 100644 dist/routes/authRoutes.js create mode 100644 dist/routes/authRoutes.js.map diff --git a/dist/app.js b/dist/app.js new file mode 100644 index 0000000..c9d61ed --- /dev/null +++ b/dist/app.js @@ -0,0 +1,30 @@ +import express from "express"; +import db_connection from "../src/database/db_connection.js"; +import "dotenv/config"; +import "./models/UserModel"; +import "./models/ArticleModel"; +import authRouter from "./routes/authRoutes.js"; +import articleRouter from "./routes/articleRoutes.js"; +export const app = express(); +const PORT = process.env.PORT || 8080; +app.use(express.json()); +app.get("/", (_req, res) => { + res.send("Hola API"); +}); +app.use("/auth", authRouter); +app.use("/article", articleRouter); +async function startServer() { + try { + // Sincroniza los modelos con la base de datos + await db_connection.sync(); // OJO: Ver las opciones más abajo + console.log("✅ Database synchronized successfully."); + app.listen(PORT, () => { + console.log(`🚀 Server is running on port ${PORT}`); + }); + } + catch (error) { + console.error("❌ Unable to sync database:", error); + } +} +startServer(); +//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/dist/app.js.map b/dist/app.js.map new file mode 100644 index 0000000..fd95ab2 --- /dev/null +++ b/dist/app.js.map @@ -0,0 +1 @@ +{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,aAAa,MAAM,kCAAkC,CAAC;AAC7D,OAAO,eAAe,CAAC;AACvB,OAAO,oBAAoB,CAAC;AAC5B,OAAO,uBAAuB,CAAC;AAC/B,OAAO,UAAU,MAAM,wBAAwB,CAAC;AAChD,OAAO,aAAa,MAAM,2BAA2B,CAAC;AAErD,MAAM,CAAC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACxB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC1B,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AACH,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,CAAE,CAAA;AAC7B,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;AAElC,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC;QACH,8CAA8C;QAC9C,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC,kCAAkC;QAC9D,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;QAErD,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;YACpB,OAAO,CAAC,GAAG,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;AACH,CAAC;AAED,WAAW,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/controllers/ArticleController.js b/dist/controllers/ArticleController.js new file mode 100644 index 0000000..aa89a4c --- /dev/null +++ b/dist/controllers/ArticleController.js @@ -0,0 +1,80 @@ +import { Article } from "../models/ArticleModel.js"; +export const getAllArticles = async (_req, res) => { + try { + const articles = await Article.findAll(); + res.status(200).json(articles); + } + catch (error) { + res.status(500).json({ message: "Error obteniendo artículos", error }); + } +}; +export const getArticleById = async (req, res) => { + try { + const { id } = req.params; + // Buscamos por clave primaria con Sequelize (findByPk) + const article = await Article.findByPk(id); + // Si no existe, respondemos 404 Not Found + if (!article) { + return res.status(404).json({ message: "Artículo no encontrado" }); + } + // Si existe, 200 OK con el artículo + res.status(200).json(article); + } + catch (error) { + // Cualquier error inesperado -> 500 + res.status(500).json({ message: "Error obteniendo el artículo" }); + } +}; +export const deleteArticle = async (req, res) => { + try { + const deleted = await Article.destroy({ where: { id: req.params.id } }); + if (deleted === 0) { + return res.status(404).json({ message: "Artículo no encontrado" }); + } + res.status(200).json({ message: "El articulo esta eliminado correctamente" }); + } + catch (error) { + res.status(500).json({ message: "No se pudo eliminar el articulo" }); + } +}; +export const createArticle = async (req, res) => { + try { + // Aquí filtramos los campos que sí queremos guardar + const { title, description, content, category, species, image, references, creator_id, } = req.body; + // Creamos el artículo solo con esos campos (los demás se ignoran) + const newArticle = await Article.create({ + title, + description, + content, + category, + species, + image, + references, + creator_id, + }); + return res.status(201).json(newArticle); + } + catch (error) { + return res.status(500).json({ message: "No se pudo crear el artículo" }); + } +}; +export const updateArticle = async (req, res) => { + try { + const { id } = req.params; + const article = await Article.findByPk(id); + if (!article) { + return res.status(404).json({ message: "Artículo no encontrado" }); + } + // Filtramos los campos que pueden actualizarse + const { title, description, content, category, species, image, references } = req.body; + await article.update({ title, description, content, category, species, image, references, }); + return res.status(200).json({ + message: "Artículo actualizado correctamente", + article, + }); + } + catch (_error) { + return res.status(500).json({ message: "Error actualizando el artículo" }); + } +}; +//# sourceMappingURL=ArticleController.js.map \ No newline at end of file diff --git a/dist/controllers/ArticleController.js.map b/dist/controllers/ArticleController.js.map new file mode 100644 index 0000000..243105e --- /dev/null +++ b/dist/controllers/ArticleController.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ArticleController.js","sourceRoot":"","sources":["../../src/controllers/ArticleController.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/controllers/AuthController.js b/dist/controllers/AuthController.js new file mode 100644 index 0000000..9122f10 --- /dev/null +++ b/dist/controllers/AuthController.js @@ -0,0 +1,73 @@ +import bcrypt from "bcryptjs"; +import UserModel from "../models/UserModel.js"; +// 👉 Registro de usuarios +export const registerController = async (req, res) => { + try { + // 1. Desestructura los nuevos campos + const { username, name, last_name, email, password, role } = req.body; + // 2. Añádelos a la validación + if (!username || !name || !last_name || !email || !password) { + res.status(400).json({ message: "Todos los campos son requeridos" }); + return; + } + // Normalizamos email + const normalizedEmail = email.toLowerCase().trim(); + // verificar si el email ya existe + const existingUser = await UserModel.findOne({ where: { email: normalizedEmail } }); + if (existingUser) { + res.status(409).json({ message: "El email ya está registrado" }); + return; + } + // hashear contraseña + const hashPassword = await bcrypt.hash(password, 10); + // 3. Pasa todos los campos requeridos a .create() + const newUser = await UserModel.create({ + username, + name, + last_name, + email: normalizedEmail, + password: hashPassword, + role + }); + res.status(201).json({ + message: "Usuario registrado exitosamente", + userId: newUser.id, + }); + } + catch (error) { + res.status(500).json({ + message: error instanceof Error ? error.message : "Unexpected error", + }); + } +}; +// 👉 Login de usuarios +export const loginController = async (req, res) => { + try { + const { email, password } = req.body; + if (!email || !password) { + res.status(400).json({ message: "Email y password son requeridos" }); + return; + } + // Normalizamos email + const normalizedEmail = email.toLowerCase().trim(); + // buscar usuario por email normalizado + const user = await UserModel.findOne({ where: { email: normalizedEmail } }); + if (!user) { + res.status(404).json({ message: "Usuario no encontrado" }); + return; + } + // comparar contraseña ingresada con la hasheada en BD + const ok = await bcrypt.compare(password, user.password); + if (!ok) { + res.status(401).json({ message: "Contraseña incorrecta" }); + return; + } + res.status(200).json({ message: "Login exitoso" }); + } + catch (error) { + res.status(500).json({ + message: error instanceof Error ? error.message : "Unexpected error", + }); + } +}; +//# sourceMappingURL=AuthController.js.map \ No newline at end of file diff --git a/dist/controllers/AuthController.js.map b/dist/controllers/AuthController.js.map new file mode 100644 index 0000000..175f977 --- /dev/null +++ b/dist/controllers/AuthController.js.map @@ -0,0 +1 @@ +{"version":3,"file":"AuthController.js","sourceRoot":"","sources":["../../src/controllers/AuthController.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/database/db_connection.js b/dist/database/db_connection.js new file mode 100644 index 0000000..e3ee762 --- /dev/null +++ b/dist/database/db_connection.js @@ -0,0 +1,12 @@ +import { Sequelize } from "sequelize"; +import dotenv from "dotenv"; +dotenv.config(); +const db_connection = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, { + host: "localhost", + dialect: "mysql", + define: { + timestamps: false, //esta parte es un añadido por lo de createAT y updateAt + }, +}); +export default db_connection; +//# sourceMappingURL=db_connection.js.map \ No newline at end of file diff --git a/dist/database/db_connection.js.map b/dist/database/db_connection.js.map new file mode 100644 index 0000000..5342aaa --- /dev/null +++ b/dist/database/db_connection.js.map @@ -0,0 +1 @@ +{"version":3,"file":"db_connection.js","sourceRoot":"","sources":["../../src/database/db_connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,aAAa,GAAG,IAAI,SAAS,CACjC,OAAO,CAAC,GAAG,CAAC,OAAiB,EAC7B,OAAO,CAAC,GAAG,CAAC,OAAiB,EAC7B,OAAO,CAAC,GAAG,CAAC,OAAiB,EAC7B;IACE,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE;QACN,UAAU,EAAE,KAAK,EAAE,wDAAwD;KAC5E;CACF,CACF,CAAC;AAEF,eAAe,aAAa,CAAC"} \ No newline at end of file diff --git a/dist/models/ArticleModel.js b/dist/models/ArticleModel.js new file mode 100644 index 0000000..2c1a06c --- /dev/null +++ b/dist/models/ArticleModel.js @@ -0,0 +1,108 @@ +// 1) Importo tipos y utilidades de Sequelize +import { DataTypes, Model } from "sequelize"; +// 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) +import db_connection from "../database/db_connection.js"; +// 5) Defino la clase del modelo (tipada) +export class Article extends Model { + id; + creator_id; + title; + description; + content; + category; + species; + image; + references; + created_at; + updated_at; +} +// 6) Inicializo (equivalente a define) y mapeo columnas/validaciones +Article.init({ + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "titulo no puede estar vacío" }, + len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, + }, + }, + description: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "descripcion no puede estar vacío" }, + len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, + }, + }, + content: { + type: DataTypes.TEXT("long"), + allowNull: false, + validate: { + notNull: { msg: "content no puede estar vacío" }, + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + category: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "content no puede estar vacío" }, + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + species: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "content no puede estar vacío" }, + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + image: { + type: DataTypes.STRING, + allowNull: true, + validate: { + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + references: { + type: DataTypes.STRING, + allowNull: true, + validate: { + len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + }, + }, + creator_id: { + type: DataTypes.BIGINT, + allowNull: false, + references: { + model: 'users', + key: 'id' + }, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, +}, { + sequelize: db_connection, // ← tu conexión + tableName: "articles", // ← nombre real de la tabla + timestamps: true, // ← activa created/updated + underscored: true, // ← columnas snake_case + createdAt: "created_at", // ← mapea el nombre + updatedAt: "updated_at", // ← mapea el nombre +}); +// 7) ¡Exporto el modelo! (puedes default o nombrado) +export default Article; +//# sourceMappingURL=ArticleModel.js.map \ No newline at end of file diff --git a/dist/models/ArticleModel.js.map b/dist/models/ArticleModel.js.map new file mode 100644 index 0000000..c416394 --- /dev/null +++ b/dist/models/ArticleModel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ArticleModel.js","sourceRoot":"","sources":["../../src/models/ArticleModel.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/models/UserModel.js b/dist/models/UserModel.js new file mode 100644 index 0000000..8721e76 --- /dev/null +++ b/dist/models/UserModel.js @@ -0,0 +1,82 @@ +// 1) Importo tipos y utilidades de Sequelize +import { DataTypes, Model } from "sequelize"; +// 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) +import db_connection from "../database/db_connection.js"; +// 5) Defino la clase del modelo (tipada) +export class User extends Model { + id; + username; + email; + password; + name; + last_name; + role; + created_at; + updated_at; +} +// 6) Inicializo (equivalente a define) y mapeo columnas/validaciones +User.init({ + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "username no puede estar vacío" }, + len: { args: [2, 255], msg: "username mínimo 2 caracteres" }, + }, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + notNull: { msg: "email no puede estar vacío" }, + isEmail: { msg: "email no es válido" }, + }, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notNull: { msg: "password no puede estar vacío" }, + len: { args: [6, 255], msg: "password mínimo 6 caracteres" }, + }, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + last_name: { + type: DataTypes.STRING, + allowNull: false, + }, + role: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "user", + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, +}, { + sequelize: db_connection, // ← tu conexión + tableName: "users", // ← nombre real de la tabla + timestamps: true, // ← activa created/updated + underscored: true, // ← columnas snake_case + createdAt: "created_at", // ← mapea el nombre + updatedAt: "updated_at", // ← mapea el nombre +}); +// 7) ¡Exporto el modelo! (puedes default o nombrado) +export default User; +//# sourceMappingURL=UserModel.js.map \ No newline at end of file diff --git a/dist/models/UserModel.js.map b/dist/models/UserModel.js.map new file mode 100644 index 0000000..46f20da --- /dev/null +++ b/dist/models/UserModel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"UserModel.js","sourceRoot":"","sources":["../../src/models/UserModel.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAY,MAAM,WAAW,CAAC;AACvD,0EAA0E;AAC1E,OAAO,aAAa,MAAM,8BAA8B,CAAA;AAqBxD,yCAAyC;AACzC,MAAM,OAAO,IACX,SAAQ,KAA6C;IAGpD,EAAE,CAAU;IACZ,QAAQ,CAAU;IAClB,KAAK,CAAU;IACf,QAAQ,CAAU;IAClB,IAAI,CAAU;IACd,SAAS,CAAU;IACnB,IAAI,CAAU;IACd,UAAU,CAAQ;IAClB,UAAU,CAAQ;CACpB;AAED,qEAAqE;AACrE,IAAI,CAAC,IAAI,CACP;IACE,EAAE,EAAE;QACF,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,aAAa,EAAE,IAAI;QACnB,UAAU,EAAE,IAAI;KACjB;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE;YACR,OAAO,EAAE,EAAE,GAAG,EAAE,+BAA+B,EAAE;YACjD,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE;SAC7D;KACF;IACD,KAAK,EAAE;QACL,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE;YACR,OAAO,EAAE,EAAE,GAAG,EAAE,4BAA4B,EAAE;YAC9C,OAAO,EAAE,EAAE,GAAG,EAAE,oBAAoB,EAAE;SACvC;KACF;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE;YACR,OAAO,EAAE,EAAE,GAAG,EAAE,+BAA+B,EAAE;YACjD,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE;SAC7D;KACF;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;KACjB;IACD,SAAS,EAAE;QACT,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;KACjB;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,MAAM;KACrB;IACD,UAAU,EAAE;QACV,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,SAAS,CAAC,GAAG;KAC5B;IACD,UAAU,EAAE;QACV,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,SAAS,CAAC,GAAG;KAC5B;CACF,EACD;IACE,SAAS,EAAE,aAAa,EAAY,gBAAgB;IACpD,SAAS,EAAE,OAAO,EAAO,4BAA4B;IACrD,UAAU,EAAE,IAAI,EAAS,2BAA2B;IACpD,WAAW,EAAE,IAAI,EAAQ,wBAAwB;IACjD,SAAS,EAAE,YAAY,EAAE,oBAAoB;IAC7C,SAAS,EAAE,YAAY,EAAE,oBAAoB;CAC9C,CACF,CAAC;AAEF,qDAAqD;AACrD,eAAe,IAAI,CAAC"} \ No newline at end of file diff --git a/dist/routes/articleRoutes.js b/dist/routes/articleRoutes.js new file mode 100644 index 0000000..c825526 --- /dev/null +++ b/dist/routes/articleRoutes.js @@ -0,0 +1,10 @@ +import express from 'express'; +import { getAllArticles, getArticleById, deleteArticle, createArticle, updateArticle } from '../controllers/ArticleController.js'; +const articleRouter = express.Router(); +articleRouter.get("/", getAllArticles); +articleRouter.get("/:id", getArticleById); +articleRouter.post("/", createArticle); +articleRouter.delete("/:id", deleteArticle); +articleRouter.put("/:id", updateArticle); +export default articleRouter; +//# sourceMappingURL=articleRoutes.js.map \ No newline at end of file diff --git a/dist/routes/articleRoutes.js.map b/dist/routes/articleRoutes.js.map new file mode 100644 index 0000000..43e7066 --- /dev/null +++ b/dist/routes/articleRoutes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"articleRoutes.js","sourceRoot":"","sources":["../../src/routes/articleRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAC,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAG,aAAa,EAAC,MAAM,qCAAqC,CAAA;AAChI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAA;AAEtC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CAAA;AAEtC,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;AAEzC,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;AAEtC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE3C,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAExC,eAAe,aAAa,CAAA"} \ No newline at end of file diff --git a/dist/routes/authRoutes.js b/dist/routes/authRoutes.js new file mode 100644 index 0000000..8caf3e1 --- /dev/null +++ b/dist/routes/authRoutes.js @@ -0,0 +1,7 @@ +import express from 'express'; +import { registerController, loginController } from '../controllers/AuthController.js'; +const authRouter = express.Router(); +authRouter.post("/register", registerController); +authRouter.post("/login", loginController); +export default authRouter; +//# sourceMappingURL=authRoutes.js.map \ No newline at end of file diff --git a/dist/routes/authRoutes.js.map b/dist/routes/authRoutes.js.map new file mode 100644 index 0000000..46ad23b --- /dev/null +++ b/dist/routes/authRoutes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"authRoutes.js","sourceRoot":"","sources":["../../src/routes/authRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AACtF,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAA;AAGnC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAA;AAChD,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAA;AAE1C,eAAe,UAAU,CAAA"} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 1e429ec..812977c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,7 +3,8 @@ import db_connection from "../src/database/db_connection.js"; import "dotenv/config"; import "./models/UserModel"; import "./models/ArticleModel"; - +import authRouter from "./routes/authRoutes.js"; +import articleRouter from "./routes/articleRoutes.js"; export const app = express(); const PORT = process.env.PORT || 8080; @@ -12,6 +13,8 @@ import "./models/ArticleModel"; app.get("/", (_req, res) => { res.send("Hola API"); }); +app.use("/auth", authRouter ) +app.use("/article", articleRouter) async function startServer() { try { diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts index a4cbf03..3bbb455 100644 --- a/src/models/ArticleModel.ts +++ b/src/models/ArticleModel.ts @@ -30,17 +30,17 @@ export class Article extends Model implements ArticleAttributes { - public id!: bigint; - public creator_id!: bigint; - public title!: string; - public description!: string; - public content!: string; - public category!: string; - public species!: string; - public image!: string; - public references!: string; - public created_at!: Date; - public updated_at!: Date; + declare id: bigint; + declare creator_id: bigint; + declare title: string; + declare description: string; + declare content: string; + declare category: string; + declare species: string; + declare image: string; + declare references: string; + declare created_at: Date; + declare updated_at: Date; } diff --git a/src/models/UserModel.ts b/src/models/UserModel.ts index a2d55d9..bc6f33c 100644 --- a/src/models/UserModel.ts +++ b/src/models/UserModel.ts @@ -27,15 +27,15 @@ export class User extends Model implements UserAttributes { - public id!: bigint; - public username!: string; - public email!: string; - public password!: string; - public name!: string; - public last_name!: string; - public role!: string; - public created_at!: Date; - public updated_at!: Date; + declare id: bigint; + declare username: string; + declare email: string; + declare password: string; + declare name: string; + declare last_name: string; + declare role: string; + declare created_at: Date; + declare updated_at: Date; } // 6) Inicializo (equivalente a define) y mapeo columnas/validaciones From a197145d8979c20861367bc96f23272f9fce1a7b Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Tue, 30 Sep 2025 12:20:43 +0200 Subject: [PATCH 11/55] chore: remove dist folder from repository and add to .gitignore --- dist/app.js | 30 ------ dist/app.js.map | 1 - dist/controllers/ArticleController.js | 80 ---------------- dist/controllers/ArticleController.js.map | 1 - dist/controllers/AuthController.js | 73 --------------- dist/controllers/AuthController.js.map | 1 - dist/database/db_connection.js | 12 --- dist/database/db_connection.js.map | 1 - dist/models/ArticleModel.js | 108 ---------------------- dist/models/ArticleModel.js.map | 1 - dist/models/UserModel.js | 82 ---------------- dist/models/UserModel.js.map | 1 - dist/routes/articleRoutes.js | 10 -- dist/routes/articleRoutes.js.map | 1 - dist/routes/authRoutes.js | 7 -- dist/routes/authRoutes.js.map | 1 - 16 files changed, 410 deletions(-) delete mode 100644 dist/app.js delete mode 100644 dist/app.js.map delete mode 100644 dist/controllers/ArticleController.js delete mode 100644 dist/controllers/ArticleController.js.map delete mode 100644 dist/controllers/AuthController.js delete mode 100644 dist/controllers/AuthController.js.map delete mode 100644 dist/database/db_connection.js delete mode 100644 dist/database/db_connection.js.map delete mode 100644 dist/models/ArticleModel.js delete mode 100644 dist/models/ArticleModel.js.map delete mode 100644 dist/models/UserModel.js delete mode 100644 dist/models/UserModel.js.map delete mode 100644 dist/routes/articleRoutes.js delete mode 100644 dist/routes/articleRoutes.js.map delete mode 100644 dist/routes/authRoutes.js delete mode 100644 dist/routes/authRoutes.js.map diff --git a/dist/app.js b/dist/app.js deleted file mode 100644 index c9d61ed..0000000 --- a/dist/app.js +++ /dev/null @@ -1,30 +0,0 @@ -import express from "express"; -import db_connection from "../src/database/db_connection.js"; -import "dotenv/config"; -import "./models/UserModel"; -import "./models/ArticleModel"; -import authRouter from "./routes/authRoutes.js"; -import articleRouter from "./routes/articleRoutes.js"; -export const app = express(); -const PORT = process.env.PORT || 8080; -app.use(express.json()); -app.get("/", (_req, res) => { - res.send("Hola API"); -}); -app.use("/auth", authRouter); -app.use("/article", articleRouter); -async function startServer() { - try { - // Sincroniza los modelos con la base de datos - await db_connection.sync(); // OJO: Ver las opciones más abajo - console.log("✅ Database synchronized successfully."); - app.listen(PORT, () => { - console.log(`🚀 Server is running on port ${PORT}`); - }); - } - catch (error) { - console.error("❌ Unable to sync database:", error); - } -} -startServer(); -//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/dist/app.js.map b/dist/app.js.map deleted file mode 100644 index fd95ab2..0000000 --- a/dist/app.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,aAAa,MAAM,kCAAkC,CAAC;AAC7D,OAAO,eAAe,CAAC;AACvB,OAAO,oBAAoB,CAAC;AAC5B,OAAO,uBAAuB,CAAC;AAC/B,OAAO,UAAU,MAAM,wBAAwB,CAAC;AAChD,OAAO,aAAa,MAAM,2BAA2B,CAAC;AAErD,MAAM,CAAC,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AACxB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC1B,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AACH,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,CAAE,CAAA;AAC7B,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;AAElC,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC;QACH,8CAA8C;QAC9C,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC,kCAAkC;QAC9D,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;QAErD,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;YACpB,OAAO,CAAC,GAAG,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;AACH,CAAC;AAED,WAAW,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/controllers/ArticleController.js b/dist/controllers/ArticleController.js deleted file mode 100644 index aa89a4c..0000000 --- a/dist/controllers/ArticleController.js +++ /dev/null @@ -1,80 +0,0 @@ -import { Article } from "../models/ArticleModel.js"; -export const getAllArticles = async (_req, res) => { - try { - const articles = await Article.findAll(); - res.status(200).json(articles); - } - catch (error) { - res.status(500).json({ message: "Error obteniendo artículos", error }); - } -}; -export const getArticleById = async (req, res) => { - try { - const { id } = req.params; - // Buscamos por clave primaria con Sequelize (findByPk) - const article = await Article.findByPk(id); - // Si no existe, respondemos 404 Not Found - if (!article) { - return res.status(404).json({ message: "Artículo no encontrado" }); - } - // Si existe, 200 OK con el artículo - res.status(200).json(article); - } - catch (error) { - // Cualquier error inesperado -> 500 - res.status(500).json({ message: "Error obteniendo el artículo" }); - } -}; -export const deleteArticle = async (req, res) => { - try { - const deleted = await Article.destroy({ where: { id: req.params.id } }); - if (deleted === 0) { - return res.status(404).json({ message: "Artículo no encontrado" }); - } - res.status(200).json({ message: "El articulo esta eliminado correctamente" }); - } - catch (error) { - res.status(500).json({ message: "No se pudo eliminar el articulo" }); - } -}; -export const createArticle = async (req, res) => { - try { - // Aquí filtramos los campos que sí queremos guardar - const { title, description, content, category, species, image, references, creator_id, } = req.body; - // Creamos el artículo solo con esos campos (los demás se ignoran) - const newArticle = await Article.create({ - title, - description, - content, - category, - species, - image, - references, - creator_id, - }); - return res.status(201).json(newArticle); - } - catch (error) { - return res.status(500).json({ message: "No se pudo crear el artículo" }); - } -}; -export const updateArticle = async (req, res) => { - try { - const { id } = req.params; - const article = await Article.findByPk(id); - if (!article) { - return res.status(404).json({ message: "Artículo no encontrado" }); - } - // Filtramos los campos que pueden actualizarse - const { title, description, content, category, species, image, references } = req.body; - await article.update({ title, description, content, category, species, image, references, }); - return res.status(200).json({ - message: "Artículo actualizado correctamente", - article, - }); - } - catch (_error) { - return res.status(500).json({ message: "Error actualizando el artículo" }); - } -}; -//# sourceMappingURL=ArticleController.js.map \ No newline at end of file diff --git a/dist/controllers/ArticleController.js.map b/dist/controllers/ArticleController.js.map deleted file mode 100644 index 243105e..0000000 --- a/dist/controllers/ArticleController.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"ArticleController.js","sourceRoot":"","sources":["../../src/controllers/ArticleController.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/controllers/AuthController.js b/dist/controllers/AuthController.js deleted file mode 100644 index 9122f10..0000000 --- a/dist/controllers/AuthController.js +++ /dev/null @@ -1,73 +0,0 @@ -import bcrypt from "bcryptjs"; -import UserModel from "../models/UserModel.js"; -// 👉 Registro de usuarios -export const registerController = async (req, res) => { - try { - // 1. Desestructura los nuevos campos - const { username, name, last_name, email, password, role } = req.body; - // 2. Añádelos a la validación - if (!username || !name || !last_name || !email || !password) { - res.status(400).json({ message: "Todos los campos son requeridos" }); - return; - } - // Normalizamos email - const normalizedEmail = email.toLowerCase().trim(); - // verificar si el email ya existe - const existingUser = await UserModel.findOne({ where: { email: normalizedEmail } }); - if (existingUser) { - res.status(409).json({ message: "El email ya está registrado" }); - return; - } - // hashear contraseña - const hashPassword = await bcrypt.hash(password, 10); - // 3. Pasa todos los campos requeridos a .create() - const newUser = await UserModel.create({ - username, - name, - last_name, - email: normalizedEmail, - password: hashPassword, - role - }); - res.status(201).json({ - message: "Usuario registrado exitosamente", - userId: newUser.id, - }); - } - catch (error) { - res.status(500).json({ - message: error instanceof Error ? error.message : "Unexpected error", - }); - } -}; -// 👉 Login de usuarios -export const loginController = async (req, res) => { - try { - const { email, password } = req.body; - if (!email || !password) { - res.status(400).json({ message: "Email y password son requeridos" }); - return; - } - // Normalizamos email - const normalizedEmail = email.toLowerCase().trim(); - // buscar usuario por email normalizado - const user = await UserModel.findOne({ where: { email: normalizedEmail } }); - if (!user) { - res.status(404).json({ message: "Usuario no encontrado" }); - return; - } - // comparar contraseña ingresada con la hasheada en BD - const ok = await bcrypt.compare(password, user.password); - if (!ok) { - res.status(401).json({ message: "Contraseña incorrecta" }); - return; - } - res.status(200).json({ message: "Login exitoso" }); - } - catch (error) { - res.status(500).json({ - message: error instanceof Error ? error.message : "Unexpected error", - }); - } -}; -//# sourceMappingURL=AuthController.js.map \ No newline at end of file diff --git a/dist/controllers/AuthController.js.map b/dist/controllers/AuthController.js.map deleted file mode 100644 index 175f977..0000000 --- a/dist/controllers/AuthController.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"AuthController.js","sourceRoot":"","sources":["../../src/controllers/AuthController.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,UAAU,CAAC;AAC9B,OAAO,SAAS,MAAM,wBAAwB,CAAC;AAgB/C,0BAA0B;AAC1B,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,GAAkC,EAClC,GAAa,EACE,EAAE;IACjB,IAAI,CAAC;QACH,qCAAqC;QACrC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QAErE,8BAA8B;QAC9B,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC,CAAC;YACrE,OAAO;QACT,CAAC;QAED,qBAAqB;QACrB,MAAM,eAAe,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAEnD,kCAAkC;QAClC,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC;QACpF,IAAI,YAAY,EAAE,CAAC;YACjB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,CAAC;YACjE,OAAO;QACT,CAAC;QAED,qBAAqB;QACrB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAErD,kDAAkD;QAClD,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC;YACrC,QAAQ;YACR,IAAI;YACJ,SAAS;YACT,KAAK,EAAE,eAAe;YACtB,QAAQ,EAAE,YAAY;YACtB,IAAI;SACL,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,iCAAiC;YAC1C,MAAM,EAAE,OAAO,CAAC,EAAE;SACnB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;SACrE,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC;AAEF,uBAAuB;AACvB,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,GAA+B,EAC/B,GAAa,EACE,EAAE;IACjB,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;QAErC,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;YACxB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC,CAAC;YACrE,OAAO;QACT,CAAC;QAED,qBAAqB;QACrB,MAAM,eAAe,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAEnD,uCAAuC;QACvC,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC;QAC5E,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,sDAAsD;QACtD,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;SACrE,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/database/db_connection.js b/dist/database/db_connection.js deleted file mode 100644 index e3ee762..0000000 --- a/dist/database/db_connection.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Sequelize } from "sequelize"; -import dotenv from "dotenv"; -dotenv.config(); -const db_connection = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, { - host: "localhost", - dialect: "mysql", - define: { - timestamps: false, //esta parte es un añadido por lo de createAT y updateAt - }, -}); -export default db_connection; -//# sourceMappingURL=db_connection.js.map \ No newline at end of file diff --git a/dist/database/db_connection.js.map b/dist/database/db_connection.js.map deleted file mode 100644 index 5342aaa..0000000 --- a/dist/database/db_connection.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"db_connection.js","sourceRoot":"","sources":["../../src/database/db_connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,aAAa,GAAG,IAAI,SAAS,CACjC,OAAO,CAAC,GAAG,CAAC,OAAiB,EAC7B,OAAO,CAAC,GAAG,CAAC,OAAiB,EAC7B,OAAO,CAAC,GAAG,CAAC,OAAiB,EAC7B;IACE,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE;QACN,UAAU,EAAE,KAAK,EAAE,wDAAwD;KAC5E;CACF,CACF,CAAC;AAEF,eAAe,aAAa,CAAC"} \ No newline at end of file diff --git a/dist/models/ArticleModel.js b/dist/models/ArticleModel.js deleted file mode 100644 index 2c1a06c..0000000 --- a/dist/models/ArticleModel.js +++ /dev/null @@ -1,108 +0,0 @@ -// 1) Importo tipos y utilidades de Sequelize -import { DataTypes, Model } from "sequelize"; -// 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) -import db_connection from "../database/db_connection.js"; -// 5) Defino la clase del modelo (tipada) -export class Article extends Model { - id; - creator_id; - title; - description; - content; - category; - species; - image; - references; - created_at; - updated_at; -} -// 6) Inicializo (equivalente a define) y mapeo columnas/validaciones -Article.init({ - id: { - type: DataTypes.BIGINT, - autoIncrement: true, - primaryKey: true, - }, - title: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "titulo no puede estar vacío" }, - len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, - }, - }, - description: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "descripcion no puede estar vacío" }, - len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, - }, - }, - content: { - type: DataTypes.TEXT("long"), - allowNull: false, - validate: { - notNull: { msg: "content no puede estar vacío" }, - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, - }, - }, - category: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "content no puede estar vacío" }, - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, - }, - }, - species: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "content no puede estar vacío" }, - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, - }, - }, - image: { - type: DataTypes.STRING, - allowNull: true, - validate: { - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, - }, - }, - references: { - type: DataTypes.STRING, - allowNull: true, - validate: { - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, - }, - }, - creator_id: { - type: DataTypes.BIGINT, - allowNull: false, - references: { - model: 'users', - key: 'id' - }, - }, - created_at: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW, - }, - updated_at: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW, - }, -}, { - sequelize: db_connection, // ← tu conexión - tableName: "articles", // ← nombre real de la tabla - timestamps: true, // ← activa created/updated - underscored: true, // ← columnas snake_case - createdAt: "created_at", // ← mapea el nombre - updatedAt: "updated_at", // ← mapea el nombre -}); -// 7) ¡Exporto el modelo! (puedes default o nombrado) -export default Article; -//# sourceMappingURL=ArticleModel.js.map \ No newline at end of file diff --git a/dist/models/ArticleModel.js.map b/dist/models/ArticleModel.js.map deleted file mode 100644 index c416394..0000000 --- a/dist/models/ArticleModel.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"ArticleModel.js","sourceRoot":"","sources":["../../src/models/ArticleModel.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/models/UserModel.js b/dist/models/UserModel.js deleted file mode 100644 index 8721e76..0000000 --- a/dist/models/UserModel.js +++ /dev/null @@ -1,82 +0,0 @@ -// 1) Importo tipos y utilidades de Sequelize -import { DataTypes, Model } from "sequelize"; -// 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) -import db_connection from "../database/db_connection.js"; -// 5) Defino la clase del modelo (tipada) -export class User extends Model { - id; - username; - email; - password; - name; - last_name; - role; - created_at; - updated_at; -} -// 6) Inicializo (equivalente a define) y mapeo columnas/validaciones -User.init({ - id: { - type: DataTypes.BIGINT, - autoIncrement: true, - primaryKey: true, - }, - username: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "username no puede estar vacío" }, - len: { args: [2, 255], msg: "username mínimo 2 caracteres" }, - }, - }, - email: { - type: DataTypes.STRING, - allowNull: false, - unique: true, - validate: { - notNull: { msg: "email no puede estar vacío" }, - isEmail: { msg: "email no es válido" }, - }, - }, - password: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "password no puede estar vacío" }, - len: { args: [6, 255], msg: "password mínimo 6 caracteres" }, - }, - }, - name: { - type: DataTypes.STRING, - allowNull: false, - }, - last_name: { - type: DataTypes.STRING, - allowNull: false, - }, - role: { - type: DataTypes.STRING, - allowNull: false, - defaultValue: "user", - }, - created_at: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW, - }, - updated_at: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW, - }, -}, { - sequelize: db_connection, // ← tu conexión - tableName: "users", // ← nombre real de la tabla - timestamps: true, // ← activa created/updated - underscored: true, // ← columnas snake_case - createdAt: "created_at", // ← mapea el nombre - updatedAt: "updated_at", // ← mapea el nombre -}); -// 7) ¡Exporto el modelo! (puedes default o nombrado) -export default User; -//# sourceMappingURL=UserModel.js.map \ No newline at end of file diff --git a/dist/models/UserModel.js.map b/dist/models/UserModel.js.map deleted file mode 100644 index 46f20da..0000000 --- a/dist/models/UserModel.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"UserModel.js","sourceRoot":"","sources":["../../src/models/UserModel.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAY,MAAM,WAAW,CAAC;AACvD,0EAA0E;AAC1E,OAAO,aAAa,MAAM,8BAA8B,CAAA;AAqBxD,yCAAyC;AACzC,MAAM,OAAO,IACX,SAAQ,KAA6C;IAGpD,EAAE,CAAU;IACZ,QAAQ,CAAU;IAClB,KAAK,CAAU;IACf,QAAQ,CAAU;IAClB,IAAI,CAAU;IACd,SAAS,CAAU;IACnB,IAAI,CAAU;IACd,UAAU,CAAQ;IAClB,UAAU,CAAQ;CACpB;AAED,qEAAqE;AACrE,IAAI,CAAC,IAAI,CACP;IACE,EAAE,EAAE;QACF,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,aAAa,EAAE,IAAI;QACnB,UAAU,EAAE,IAAI;KACjB;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE;YACR,OAAO,EAAE,EAAE,GAAG,EAAE,+BAA+B,EAAE;YACjD,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE;SAC7D;KACF;IACD,KAAK,EAAE;QACL,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE;YACR,OAAO,EAAE,EAAE,GAAG,EAAE,4BAA4B,EAAE;YAC9C,OAAO,EAAE,EAAE,GAAG,EAAE,oBAAoB,EAAE;SACvC;KACF;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE;YACR,OAAO,EAAE,EAAE,GAAG,EAAE,+BAA+B,EAAE;YACjD,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,8BAA8B,EAAE;SAC7D;KACF;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;KACjB;IACD,SAAS,EAAE;QACT,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;KACjB;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,MAAM;KACrB;IACD,UAAU,EAAE;QACV,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,SAAS,CAAC,GAAG;KAC5B;IACD,UAAU,EAAE;QACV,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,SAAS,EAAE,KAAK;QAChB,YAAY,EAAE,SAAS,CAAC,GAAG;KAC5B;CACF,EACD;IACE,SAAS,EAAE,aAAa,EAAY,gBAAgB;IACpD,SAAS,EAAE,OAAO,EAAO,4BAA4B;IACrD,UAAU,EAAE,IAAI,EAAS,2BAA2B;IACpD,WAAW,EAAE,IAAI,EAAQ,wBAAwB;IACjD,SAAS,EAAE,YAAY,EAAE,oBAAoB;IAC7C,SAAS,EAAE,YAAY,EAAE,oBAAoB;CAC9C,CACF,CAAC;AAEF,qDAAqD;AACrD,eAAe,IAAI,CAAC"} \ No newline at end of file diff --git a/dist/routes/articleRoutes.js b/dist/routes/articleRoutes.js deleted file mode 100644 index c825526..0000000 --- a/dist/routes/articleRoutes.js +++ /dev/null @@ -1,10 +0,0 @@ -import express from 'express'; -import { getAllArticles, getArticleById, deleteArticle, createArticle, updateArticle } from '../controllers/ArticleController.js'; -const articleRouter = express.Router(); -articleRouter.get("/", getAllArticles); -articleRouter.get("/:id", getArticleById); -articleRouter.post("/", createArticle); -articleRouter.delete("/:id", deleteArticle); -articleRouter.put("/:id", updateArticle); -export default articleRouter; -//# sourceMappingURL=articleRoutes.js.map \ No newline at end of file diff --git a/dist/routes/articleRoutes.js.map b/dist/routes/articleRoutes.js.map deleted file mode 100644 index 43e7066..0000000 --- a/dist/routes/articleRoutes.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"articleRoutes.js","sourceRoot":"","sources":["../../src/routes/articleRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAC,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAG,aAAa,EAAC,MAAM,qCAAqC,CAAA;AAChI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAA;AAEtC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CAAA;AAEtC,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;AAEzC,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;AAEtC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAE3C,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAExC,eAAe,aAAa,CAAA"} \ No newline at end of file diff --git a/dist/routes/authRoutes.js b/dist/routes/authRoutes.js deleted file mode 100644 index 8caf3e1..0000000 --- a/dist/routes/authRoutes.js +++ /dev/null @@ -1,7 +0,0 @@ -import express from 'express'; -import { registerController, loginController } from '../controllers/AuthController.js'; -const authRouter = express.Router(); -authRouter.post("/register", registerController); -authRouter.post("/login", loginController); -export default authRouter; -//# sourceMappingURL=authRoutes.js.map \ No newline at end of file diff --git a/dist/routes/authRoutes.js.map b/dist/routes/authRoutes.js.map deleted file mode 100644 index 46ad23b..0000000 --- a/dist/routes/authRoutes.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"authRoutes.js","sourceRoot":"","sources":["../../src/routes/authRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AACtF,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAA;AAGnC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAA;AAChD,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAA;AAE1C,eAAe,UAAU,CAAA"} \ No newline at end of file From ec4c25852cfb37f580e427d4258d4c76222e64f1 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Tue, 30 Sep 2025 12:22:05 +0200 Subject: [PATCH 12/55] add dist folder to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a5a95fe..3c1a83c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .env .env.test -.DS_Store \ No newline at end of file +.DS_Store +dist \ No newline at end of file From 72f424f7952e951a88bb53a41c8f713c3f5825f7 Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 30 Sep 2025 15:31:57 +0200 Subject: [PATCH 13/55] feat: implement JWT authentication and authorization --- src/controllers/AuthController.ts | 66 ++++++++++++++++++++++----- src/middlewares/auth.ts | 75 +++++++++++++++++++++++++++++++ src/routes/articleRoutes.ts | 29 +++++++----- src/utils/jwt.ts | 44 ++++++++++++++++++ 4 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 src/middlewares/auth.ts create mode 100644 src/utils/jwt.ts diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 0198d39..4d85078 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,14 +1,16 @@ + import { Request, Response } from "express"; import bcrypt from "bcryptjs"; import UserModel from "../models/UserModel.js"; +import { generateToken } from "../utils/jwt.js"; interface RegisterBody { - username: string; // Añadir + username: string; name: string; - last_name: string; // Añadir + last_name: string; email: string; password: string; - role: string; + role?: string; } interface LoginBody { @@ -22,10 +24,10 @@ export const registerController = async ( res: Response ): Promise => { try { - // 1. Desestructura los nuevos campos - const { username, name, last_name, email, password, role} = req.body; + // 1. Desestructura los campos + const { username, name, last_name, email, password, role } = req.body; - // 2. Añádelos a la validación + // 2. Validación if (!username || !name || !last_name || !email || !password) { res.status(400).json({ message: "Todos los campos son requeridos" }); return; @@ -35,7 +37,10 @@ export const registerController = async ( const normalizedEmail = email.toLowerCase().trim(); // verificar si el email ya existe - const existingUser = await UserModel.findOne({ where: { email: normalizedEmail } }); + const existingUser = await UserModel.findOne({ + where: { email: normalizedEmail } + }); + if (existingUser) { res.status(409).json({ message: "El email ya está registrado" }); return; @@ -44,19 +49,34 @@ export const registerController = async ( // hashear contraseña const hashPassword = await bcrypt.hash(password, 10); - // 3. Pasa todos los campos requeridos a .create() + // 3. Crear usuario const newUser = await UserModel.create({ username, name, last_name, email: normalizedEmail, password: hashPassword, - role + role: role || "user", // Por defecto "user" + }); + + // 4. Generar token JWT + const token = generateToken({ + userId: newUser.id, + email: newUser.email, + role: newUser.role, }); res.status(201).json({ message: "Usuario registrado exitosamente", - userId: newUser.id, + token, + user: { + id: newUser.id.toString(), + username: newUser.username, + email: newUser.email, + name: newUser.name, + last_name: newUser.last_name, + role: newUser.role, + }, }); } catch (error) { res.status(500).json({ @@ -82,7 +102,10 @@ export const loginController = async ( const normalizedEmail = email.toLowerCase().trim(); // buscar usuario por email normalizado - const user = await UserModel.findOne({ where: { email: normalizedEmail } }); + const user = await UserModel.findOne({ + where: { email: normalizedEmail } + }); + if (!user) { res.status(404).json({ message: "Usuario no encontrado" }); return; @@ -90,12 +113,31 @@ export const loginController = async ( // comparar contraseña ingresada con la hasheada en BD const ok = await bcrypt.compare(password, user.password); + if (!ok) { res.status(401).json({ message: "Contraseña incorrecta" }); return; } - res.status(200).json({ message: "Login exitoso" }); + // Generar token JWT + const token = generateToken({ + userId: user.id, + email: user.email, + role: user.role, + }); + + res.status(200).json({ + message: "Login exitoso", + token, + user: { + id: user.id.toString(), + username: user.username, + email: user.email, + name: user.name, + last_name: user.last_name, + role: user.role, + }, + }); } catch (error) { res.status(500).json({ message: error instanceof Error ? error.message : "Unexpected error", diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts new file mode 100644 index 0000000..b1d2fff --- /dev/null +++ b/src/middlewares/auth.ts @@ -0,0 +1,75 @@ +import { Request, Response, NextFunction } from "express"; +import { verifyToken, TokenPayload } from "../utils/jwt.js"; + +// Extender el tipo Request para incluir user +declare global { + namespace Express { + interface Request { + user?: TokenPayload; + } + } +} + +/** + * Middleware que verifica si el usuario está autenticado + */ +export const authMiddleware = ( + req: Request, + res: Response, + next: NextFunction +): void => { + try { + // Obtener el token del header Authorization + const authHeader = req.headers.authorization; + + if (!authHeader) { + res.status(401).json({ message: "No se proporcionó token de autenticación" }); + return; + } + + // El formato esperado es: "Bearer " + const token = authHeader.split(" ")[1]; + + if (!token) { + res.status(401).json({ message: "Formato de token inválido" }); + return; + } + + // Verificar el token + const decoded = verifyToken(token); + + if (!decoded) { + res.status(401).json({ message: "Token inválido o expirado" }); + return; + } + + // Adjuntar la información del usuario al request + req.user = decoded; + + // Continuar con el siguiente middleware o ruta + next(); + } catch (error) { + res.status(500).json({ message: "Error en la autenticación" }); + } +}; + +/** + * Middleware que verifica si el usuario tiene un rol específico + */ +export const requireRole = (...allowedRoles: string[]) => { + return (req: Request, res: Response, next: NextFunction): void => { + if (!req.user) { + res.status(401).json({ message: "Usuario no autenticado" }); + return; + } + + if (!allowedRoles.includes(req.user.role)) { + res.status(403).json({ + message: "No tienes permisos para acceder a este recurso" + }); + return; + } + + next(); + }; +}; \ No newline at end of file diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index ce8099d..87215dd 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -1,15 +1,24 @@ -import express from 'express' -import {getAllArticles, getArticleById, deleteArticle, createArticle , updateArticle} from '../controllers/ArticleController.js' -const articleRouter = express.Router() +import express from 'express'; +import { + getAllArticles, + getArticleById, + deleteArticle, + createArticle, + updateArticle +} from '../controllers/ArticleController.js'; +import { authMiddleware, requireRole } from '../middlewares/auth.js'; -articleRouter.get("/", getAllArticles) +const articleRouter = express.Router(); -articleRouter.get("/:id", getArticleById) +// Rutas públicas (sin autenticación) +articleRouter.get("/", getAllArticles); +articleRouter.get("/:id", getArticleById); -articleRouter.post("/", createArticle) +// Rutas protegidas (requieren autenticación) +articleRouter.post("/", authMiddleware, createArticle); +articleRouter.put("/:id", authMiddleware, updateArticle); +articleRouter.delete("/:id", authMiddleware, deleteArticle); -articleRouter.delete("/:id", deleteArticle) +// articleRouter.delete("/:id", authMiddleware, requireRole("admin"), deleteArticle); -articleRouter.put("/:id", updateArticle) - -export default articleRouter \ No newline at end of file +export default articleRouter; \ No newline at end of file diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000..a27e307 --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,44 @@ +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET || "default_secret_change_in_production"; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d"; + +export interface TokenPayload { + userId: bigint; + email: string; + role: string; +} + +/** + * Genera un token JWT + */ +export const generateToken = (payload: TokenPayload): string => { + // Convertir bigint a string para JWT + const sanitizedPayload = { + userId: payload.userId.toString(), + email: payload.email, + role: payload.role, + }; + + return jwt.sign(sanitizedPayload, JWT_SECRET, { + expiresIn: JWT_EXPIRES_IN, + }); +}; + +/** + * Verifica y decodifica un token JWT + */ +export const verifyToken = (token: string): TokenPayload | null => { + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + + // Convertir userId de string a bigint + return { + userId: BigInt(decoded.userId), + email: decoded.email, + role: decoded.role, + }; + } catch (error) { + return null; + } +}; \ No newline at end of file From 03aec71bedb8d3bfcb22c2e7837bc4646b10bcdb Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Wed, 1 Oct 2025 10:46:18 +0200 Subject: [PATCH 14/55] feat(auth): add user model, JWT auth, validators and middlewares - Implement User model with Sequelize (username, email, password, role, etc.) - Add register and login controllers with password hashing (bcrypt) and email normalization - Integrate JWT token generation and verification utilities - Create validation middlewares (express-validator) for register and login - Add auth middleware to protect routes and role-based access control - Update auth routes with validation and controllers --- package-lock.json | 23 ++++++++++++++++++++ package.json | 1 + src/middlewares/auth.ts | 29 ++++++++++++++++++++++++- src/routes/authRoutes.ts | 27 +++++++++++++++++------ src/validators/.gitkeep | 0 src/validators/userValidators.ts | 37 ++++++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 7 deletions(-) delete mode 100644 src/validators/.gitkeep create mode 100644 src/validators/userValidators.ts diff --git a/package-lock.json b/package-lock.json index 1a3714a..16dd2c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", "mysql2": "^3.15.1", "sequelize": "^6.37.7" @@ -3220,6 +3221,28 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-validator/node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/package.json b/package.json index ab2901a..500edf4 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", "mysql2": "^3.15.1", "sequelize": "^6.37.7" diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index b1d2fff..46306e2 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { verifyToken, TokenPayload } from "../utils/jwt.js"; +import { validationResult } from "express-validator"; // Extender el tipo Request para incluir user declare global { @@ -72,4 +73,30 @@ export const requireRole = (...allowedRoles: string[]) => { next(); }; -}; \ No newline at end of file +}; + + +export function handleValidation(req: Request, res: Response, next: NextFunction) { + const result = validationResult(req); + + if (!result.isEmpty()) { + const flatErrors = result.array({ onlyFirstError: true }).flatMap((err: any) => { + // Si es AlternativeValidationError (oneOf), aplanamos sus nestedErrors + if (err?.nestedErrors && Array.isArray(err.nestedErrors)) { + return err.nestedErrors.map((ne: any) => ({ + field: ne.path ?? ne.param ?? "unknown", + msg: ne.msg, + })); + } + // ValidationError normal + return [{ field: err.path ?? err.param ?? "unknown", msg: err.msg }]; + }); + + return res.status(422).json({ + message: "Errores de validación", + errors: flatErrors, + }); + } + + next(); +} diff --git a/src/routes/authRoutes.ts b/src/routes/authRoutes.ts index 4dda64d..0ac51a3 100644 --- a/src/routes/authRoutes.ts +++ b/src/routes/authRoutes.ts @@ -1,9 +1,24 @@ -import express from 'express' -import { registerController, loginController } from '../controllers/AuthController.js' -const authRouter = express.Router() +import express from "express"; +import { registerController, loginController } from "../controllers/AuthController.js"; +import { registerValidator, loginValidator } from "../validators/userValidators.js"; +import { handleValidation } from "../middlewares/auth.js"; +const authRouter = express.Router(); -authRouter.post("/register", registerController) -authRouter.post("/login", loginController) +// Registro con validación +authRouter.post( + "/register", + registerValidator, + handleValidation, + registerController +); -export default authRouter \ No newline at end of file +// Login con validación +authRouter.post( + "/login", + loginValidator, + handleValidation, + loginController +); + +export default authRouter; diff --git a/src/validators/.gitkeep b/src/validators/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/validators/userValidators.ts b/src/validators/userValidators.ts new file mode 100644 index 0000000..4791488 --- /dev/null +++ b/src/validators/userValidators.ts @@ -0,0 +1,37 @@ +import { body } from "express-validator"; + +const emailRule = body("email") + .trim() + .toLowerCase() + .isEmail() + .withMessage("Email inválido") + .matches(/@/) + .withMessage("El email debe contener '@'"); + +const passwordRule = body("password") + .isString() + .isLength({ min: 6, max: 255 }) + .withMessage("La contraseña debe tener al menos 6 caracteres"); + +export const registerValidator = [ + body("username") + .trim() + .isLength({ min: 2, max: 50 }) + .withMessage("username debe tener mínimo 2 caracteres"), + body("name") + .trim() + .isLength({ min: 2 }) + .withMessage("name es requerido"), + body("last_name") + .trim() + .isLength({ min: 2 }) + .withMessage("last_name es requerido"), + emailRule, + passwordRule, + body("role") + .optional() + .isIn(["user", "admin"]) + .withMessage("role debe ser 'user' o 'admin'"), +]; + +export const loginValidator = [emailRule, passwordRule]; From ee946778c5f2d3be79fe21510436de4fb6d35ac7 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Wed, 1 Oct 2025 11:03:39 +0200 Subject: [PATCH 15/55] feat(auth): enforce unique username and require minimum 8-char password - Add validation to prevent duplicate usernames on registration - Update password validator to require at least 8 characters --- src/controllers/AuthController.ts | 12 ++++++++++++ src/validators/userValidators.ts | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 4d85078..bc08c94 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -46,6 +46,18 @@ export const registerController = async ( return; } + // verificar si el usuario ya existe + const normalizedUserName = username.toLowerCase().trim(); + + const existingUsername = await UserModel.findOne({ + where: { username: normalizedUserName } + }); + + if (existingUsername) { + res.status(409).json({ message: "El usuario ya está registrado" }); + return; + } + // hashear contraseña const hashPassword = await bcrypt.hash(password, 10); diff --git a/src/validators/userValidators.ts b/src/validators/userValidators.ts index 4791488..fab7ac9 100644 --- a/src/validators/userValidators.ts +++ b/src/validators/userValidators.ts @@ -10,8 +10,8 @@ const emailRule = body("email") const passwordRule = body("password") .isString() - .isLength({ min: 6, max: 255 }) - .withMessage("La contraseña debe tener al menos 6 caracteres"); + .isLength({ min: 8, max: 255 }) + .withMessage("La contraseña debe tener al menos 8 caracteres"); export const registerValidator = [ body("username") From 7f7cd77bd0012ac37c0dc5d51c88d086d6e61e6c Mon Sep 17 00:00:00 2001 From: gemayc Date: Wed, 1 Oct 2025 13:26:47 +0200 Subject: [PATCH 16/55] feat(articles): add validator for POST and PUT requests --- package-lock.json | 23 ++++++ package.json | 1 + src/middlewares/articleMiddlewares.ts | 25 ++++++ src/routes/articleRoutes.ts | 18 ++--- src/validators/articleValidators.ts | 106 ++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 src/middlewares/articleMiddlewares.ts create mode 100644 src/validators/articleValidators.ts diff --git a/package-lock.json b/package-lock.json index 1a3714a..16dd2c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", "mysql2": "^3.15.1", "sequelize": "^6.37.7" @@ -3220,6 +3221,28 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-validator/node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/package.json b/package.json index ab2901a..500edf4 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "bcryptjs": "^3.0.2", "dotenv": "^17.2.2", "express": "^5.1.0", + "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", "mysql2": "^3.15.1", "sequelize": "^6.37.7" diff --git a/src/middlewares/articleMiddlewares.ts b/src/middlewares/articleMiddlewares.ts new file mode 100644 index 0000000..8d23703 --- /dev/null +++ b/src/middlewares/articleMiddlewares.ts @@ -0,0 +1,25 @@ +// 1) Importo lo necesario de express y express-validator +import { validationResult } from "express-validator"; +import type { Request, Response, NextFunction } from "express"; + +// 2) Middleware que revisa si hubo errores de validación +export function checkValidations(req: Request, res: Response, next: NextFunction) { + // 3) Recoge el resultado de todos los body()/param()/query() anteriores + const errors = validationResult(req); + + // 4) Si hay errores, respondemos 400 con un listado claro + if (!errors.isEmpty()) { + return res.status(400).json({ + message: "Error de validación", + // 5) Solo mostramos el primer error por campo (más limpio para el front) + errors: errors.array({ onlyFirstError: true }).map((e: any) => ({ + field: e.param, // ← NOMBRE DEL CAMPO (p.ej., "title", "id") + message: e.msg, // ← MENSAJE que pusiste con .withMessage(...) + location: e.location, // ← DONDE falló: "body" | "params" | "query" + })), + }); + } + + // 6) Si no hay errores, seguimos al siguiente middleware/controlador + next(); +} diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 87215dd..627c208 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -1,22 +1,20 @@ import express from 'express'; -import { - getAllArticles, - getArticleById, - deleteArticle, - createArticle, - updateArticle -} from '../controllers/ArticleController.js'; +import {getAllArticles,getArticleById, deleteArticle,createArticle,updateArticle} from '../controllers/ArticleController.js'; import { authMiddleware, requireRole } from '../middlewares/auth.js'; +import { createArticleValidators, updateArticleValidators, idParamValidators } from '../validators/articleValidators.js'; +import { checkValidations } from "../middlewares/articleMiddlewares.js"; + + const articleRouter = express.Router(); // Rutas públicas (sin autenticación) articleRouter.get("/", getAllArticles); -articleRouter.get("/:id", getArticleById); +articleRouter.get("/:id", getArticleById, idParamValidators,checkValidations); // Rutas protegidas (requieren autenticación) -articleRouter.post("/", authMiddleware, createArticle); -articleRouter.put("/:id", authMiddleware, updateArticle); +articleRouter.post("/", authMiddleware, createArticle, createArticleValidators, checkValidations); +articleRouter.put("/:id", authMiddleware, updateArticle, updateArticleValidators, checkValidations); articleRouter.delete("/:id", authMiddleware, deleteArticle); // articleRouter.delete("/:id", authMiddleware, requireRole("admin"), deleteArticle); diff --git a/src/validators/articleValidators.ts b/src/validators/articleValidators.ts new file mode 100644 index 0000000..1c11cbd --- /dev/null +++ b/src/validators/articleValidators.ts @@ -0,0 +1,106 @@ +import { body, param } from "express-validator"; + +export const createArticleValidators = [ + // title: string mínimo 3 + body("title") + .isString().withMessage("title debe ser un string") // ← comprueba tipo + .trim() // ← recorta espacios + .isLength({ min: 10 }).withMessage("title mínimo 10 caracteres"), + + // description: string mínimo 3 + body("description") + .isString().withMessage("description debe ser un string") + .trim() + .isLength({ min: 3 }).withMessage("description mínimo 3 caracteres"), + + // content: string mínimo 100 + body("content") + .isString().withMessage("content debe ser un string") + .trim() + .isLength({ min: 100 }).withMessage("content mínimo 100 caracteres"), + + // category: string mínimo 6 + body("category") + .isString().withMessage("category debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("category mínimo 6 caracteres"), + + // species: string mínimo 6 + body("species") + .isString().withMessage("species debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("species mínimo 6 caracteres"), + + // image: opcional, si viene debe ser URL + body("image") + .optional() // ← no obligatorio + .isURL().withMessage("image debe ser una URL válida"), + + // references: opcional, si viene mínimo 6 + body("references") + .optional() + .isString().withMessage("references debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("references mínimo 6 caracteres"), +]; + +/** + * ✅ Validadores para ACTUALIZAR artículo (PUT /articles/:id) + * - :id en params debe ser entero positivo + * - Todos los campos del body son OPCIONALES, pero si vienen, se validan. + */ +export const updateArticleValidators = [ + // Validamos el parámetro :id + param("id") + .isInt({ min: 1 }).withMessage("id debe ser un entero positivo"), + + // El resto igual que create, pero todos .optional() + body("title") + .optional() + .isString().withMessage("title debe ser un string") + .trim() + .isLength({ min: 3 }).withMessage("title mínimo 3 caracteres"), + + body("description") + .optional() + .isString().withMessage("description debe ser un string") + .trim() + .isLength({ min: 3 }).withMessage("description mínimo 3 caracteres"), + + body("content") + .optional() + .isString().withMessage("content debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("content mínimo 6 caracteres"), + + body("category") + .optional() + .isString().withMessage("category debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("category mínimo 6 caracteres"), + + body("species") + .optional() + .isString().withMessage("species debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("species mínimo 6 caracteres"), + + body("image") + .optional() + .isURL().withMessage("image debe ser una URL válida"), + + body("references") + .optional() + .isString().withMessage("references debe ser un string") + .trim() + .isLength({ min: 6 }).withMessage("references mínimo 6 caracteres"), +]; + +/** + * ✅ Validadores para OBTENER por id (GET /articles/:id) o borrar + * - Solo chequea que :id sea un entero positivo. + */ +export const idParamValidators = [ + param("id") + .isInt({ min: 1 }).withMessage("id debe ser un entero positivo"), +]; \ No newline at end of file From df6f217b44d9a3ac749cc3e3e7da15e8bbf31087 Mon Sep 17 00:00:00 2001 From: gemayc Date: Wed, 1 Oct 2025 13:49:12 +0200 Subject: [PATCH 17/55] add validators --- src/routes/articleRoutes.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 627c208..9fdb953 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -1,6 +1,6 @@ import express from 'express'; import {getAllArticles,getArticleById, deleteArticle,createArticle,updateArticle} from '../controllers/ArticleController.js'; -import { authMiddleware, requireRole } from '../middlewares/auth.js'; +import { authMiddleware, requireRole, handleValidation} from '../middlewares/auth.js'; import { createArticleValidators, updateArticleValidators, idParamValidators } from '../validators/articleValidators.js'; import { checkValidations } from "../middlewares/articleMiddlewares.js"; @@ -12,10 +12,10 @@ const articleRouter = express.Router(); articleRouter.get("/", getAllArticles); articleRouter.get("/:id", getArticleById, idParamValidators,checkValidations); -// Rutas protegidas (requieren autenticación) -articleRouter.post("/", authMiddleware, createArticle, createArticleValidators, checkValidations); -articleRouter.put("/:id", authMiddleware, updateArticle, updateArticleValidators, checkValidations); -articleRouter.delete("/:id", authMiddleware, deleteArticle); +// Rutas protegidas (handleValidation, createArticle, createArticleValidators, checkValidations); +articleRouter.post("/", authMiddleware, createArticle, authMiddleware, handleValidation, createArticleValidators,); +articleRouter.put("/:id", authMiddleware, handleValidation, updateArticle, updateArticleValidators, checkValidations); +articleRouter.delete("/:id",authMiddleware, handleValidation, deleteArticle); // articleRouter.delete("/:id", authMiddleware, requireRole("admin"), deleteArticle); From 30d0d3ae44cb619ececacf50091d7230fc176eed Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Wed, 1 Oct 2025 14:36:55 +0200 Subject: [PATCH 18/55] feat(articles): Restrict article creation to admin role Applied the middleware to the endpoint. This change ensures that only users with administrative privileges can create new articles, enhancing the application's access control. --- src/routes/articleRoutes.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 9fdb953..0e758e8 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -13,10 +13,9 @@ articleRouter.get("/", getAllArticles); articleRouter.get("/:id", getArticleById, idParamValidators,checkValidations); // Rutas protegidas (handleValidation, createArticle, createArticleValidators, checkValidations); -articleRouter.post("/", authMiddleware, createArticle, authMiddleware, handleValidation, createArticleValidators,); -articleRouter.put("/:id", authMiddleware, handleValidation, updateArticle, updateArticleValidators, checkValidations); -articleRouter.delete("/:id",authMiddleware, handleValidation, deleteArticle); +articleRouter.post("/", requireRole("admin"), authMiddleware, createArticle, authMiddleware, handleValidation, createArticleValidators,); +articleRouter.put("/:id", requireRole("admin"), authMiddleware, handleValidation, updateArticle, updateArticleValidators, checkValidations); +articleRouter.delete("/:id",requireRole("admin"), authMiddleware, handleValidation, deleteArticle); -// articleRouter.delete("/:id", authMiddleware, requireRole("admin"), deleteArticle); export default articleRouter; \ No newline at end of file From 6d9775c922675e05260fc846e948499af7732e24 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Wed, 1 Oct 2025 15:08:08 +0200 Subject: [PATCH 19/55] fix: reorder router --- src/middlewares/auth.ts | 2 +- src/routes/articleRoutes.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index 46306e2..ead0df3 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -60,7 +60,7 @@ export const authMiddleware = ( export const requireRole = (...allowedRoles: string[]) => { return (req: Request, res: Response, next: NextFunction): void => { if (!req.user) { - res.status(401).json({ message: "Usuario no autenticado" }); + res.status(401).json({ message: "Usuario no autorizado" }); return; } diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 0e758e8..9919563 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -13,9 +13,9 @@ articleRouter.get("/", getAllArticles); articleRouter.get("/:id", getArticleById, idParamValidators,checkValidations); // Rutas protegidas (handleValidation, createArticle, createArticleValidators, checkValidations); -articleRouter.post("/", requireRole("admin"), authMiddleware, createArticle, authMiddleware, handleValidation, createArticleValidators,); -articleRouter.put("/:id", requireRole("admin"), authMiddleware, handleValidation, updateArticle, updateArticleValidators, checkValidations); -articleRouter.delete("/:id",requireRole("admin"), authMiddleware, handleValidation, deleteArticle); +articleRouter.post("/", authMiddleware, requireRole("admin"), createArticle, handleValidation, createArticleValidators,); +articleRouter.put("/:id", authMiddleware, requireRole("admin"), handleValidation, updateArticle, updateArticleValidators, checkValidations); +articleRouter.delete("/:id",authMiddleware, requireRole("admin"), handleValidation, deleteArticle); export default articleRouter; \ No newline at end of file From 6bc70ed92523b4a2a176e71d487ff43a906faf59 Mon Sep 17 00:00:00 2001 From: gemayc Date: Wed, 1 Oct 2025 15:58:36 +0200 Subject: [PATCH 20/55] feat setup test --- jest.config.cjs | 26 ++ package-lock.json | 576 +++++++++++++++++----------------- package.json | 4 +- src/app.ts | 5 +- src/database/db_connection.ts | 11 +- test/article.test.ts | 0 test/setup-env.ts | 11 + 7 files changed, 340 insertions(+), 293 deletions(-) create mode 100644 jest.config.cjs create mode 100644 test/article.test.ts create mode 100644 test/setup-env.ts diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..690d542 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,26 @@ + +// import type { Config } from "jest"; + +// const config: Config = { +// preset: "ts-jest", +// testEnvironment: "node", +// testMatch: ["**/tests/**/*.test.ts"], +// verbose: true, +// // Asegura que NODE_ENV=test y cargue .env.test en los tests +// setupFiles: ["/tests/setup-env.ts"], +// }; + +// export default config; + +// jest.config.cjs +export default { + preset: "ts-jest/presets/default-esm", // usa ts-jest con ESM + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + globals: { + "ts-jest": { + useESM: true + } + } +}; + diff --git a/package-lock.json b/package-lock.json index 16dd2c7..a4f6cbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", - "jest": "^30.1.3", + "jest": "^30.2.0", "supertest": "^7.1.4", "ts-jest": "^29.4.4", "tsx": "^4.20.6", @@ -1049,17 +1049,17 @@ } }, "node_modules/@jest/console": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", - "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -1067,39 +1067,39 @@ } }, "node_modules/@jest/core": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", - "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", + "@jest/console": "30.2.0", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.1.3", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.5", - "jest-config": "30.1.3", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-resolve-dependencies": "30.1.3", - "jest-runner": "30.1.3", - "jest-runtime": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", - "jest-watcher": "30.1.3", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -1125,39 +1125,39 @@ } }, "node_modules/@jest/environment": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", - "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.1.2", - "@jest/types": "30.0.5", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5" + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", - "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.1.2", - "jest-snapshot": "30.1.2" + "expect": "30.2.0", + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", - "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, "license": "MIT", "dependencies": { @@ -1168,18 +1168,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", - "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1196,16 +1196,16 @@ } }, "node_modules/@jest/globals": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.2.tgz", - "integrity": "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/expect": "30.1.2", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1226,17 +1226,17 @@ } }, "node_modules/@jest/reporters": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", - "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -1249,9 +1249,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1282,13 +1282,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", - "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -1313,14 +1313,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", - "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -1329,15 +1329,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.3.tgz", - "integrity": "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.3", + "@jest/test-result": "30.2.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -1345,23 +1345,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", - "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", + "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", @@ -1372,9 +1372,9 @@ } }, "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -2210,16 +2210,16 @@ } }, "node_modules/babel-jest": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", - "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.1.2", + "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -2228,7 +2228,7 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "node_modules/babel-plugin-istanbul": { @@ -2252,14 +2252,12 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", "@types/babel__core": "^7.20.5" }, "engines": { @@ -2294,20 +2292,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, "node_modules/balanced-match": { @@ -3162,18 +3160,18 @@ } }, "node_modules/expect": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", - "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.1.2", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3939,16 +3937,16 @@ } }, "node_modules/jest": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", - "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.3", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", "import-local": "^3.2.0", - "jest-cli": "30.1.3" + "jest-cli": "30.2.0" }, "bin": { "jest": "bin/jest.js" @@ -3966,14 +3964,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "p-limit": "^3.1.0" }, "engines": { @@ -3981,29 +3979,29 @@ } }, "node_modules/jest-circus": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", - "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/expect": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.1.0", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-runtime": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "p-limit": "^3.1.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -4013,21 +4011,21 @@ } }, "node_modules/jest-cli": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", - "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.3", - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.1.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "yargs": "^17.7.2" }, "bin": { @@ -4046,34 +4044,34 @@ } }, "node_modules/jest-config": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", - "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.1.3", - "@jest/types": "30.0.5", - "babel-jest": "30.1.2", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.1.3", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.2", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-runner": "30.1.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -4098,25 +4096,25 @@ } }, "node_modules/jest-diff": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", - "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4127,56 +4125,56 @@ } }, "node_modules/jest-each": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", - "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" + "jest-util": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", - "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/fake-timers": "30.1.2", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.1.0" + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", - "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -4188,49 +4186,49 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", - "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", - "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.1.2", - "pretty-format": "30.0.5" + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", - "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -4239,15 +4237,15 @@ } }, "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.0.5" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4282,18 +4280,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", - "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -4302,46 +4300,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", - "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.1.2" + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", - "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", - "@jest/environment": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.2", - "jest-haste-map": "30.1.0", - "jest-leak-detector": "30.1.0", - "jest-message-util": "30.1.0", - "jest-resolve": "30.1.3", - "jest-runtime": "30.1.3", - "jest-util": "30.0.5", - "jest-watcher": "30.1.3", - "jest-worker": "30.1.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4350,32 +4348,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", - "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/fake-timers": "30.1.2", - "@jest/globals": "30.1.2", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4384,9 +4382,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", - "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "dev": true, "license": "MIT", "dependencies": { @@ -4395,20 +4393,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.1.2", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.1.2", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", - "babel-preset-current-node-syntax": "^1.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.1.2", + "expect": "30.2.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.1.2", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "pretty-format": "30.0.5", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -4430,13 +4428,13 @@ } }, "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -4461,18 +4459,18 @@ } }, "node_modules/jest-validate": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", - "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4492,19 +4490,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", - "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "string-length": "^4.0.2" }, "engines": { @@ -4512,15 +4510,15 @@ } }, "node_modules/jest-worker": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", - "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -5369,9 +5367,9 @@ } }, "node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 500edf4..4f1b5bd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "tsx src/app.ts", "build": "tsc", "start": "node dist/app.js", - "test": "jest --watchAll --no-cache" + "test": "cross-env NODE_ENV=test jest --runInBand" }, "repository": { "type": "git", @@ -37,7 +37,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", - "jest": "^30.1.3", + "jest": "^30.2.0", "supertest": "^7.1.4", "ts-jest": "^29.4.4", "tsx": "^4.20.6", diff --git a/src/app.ts b/src/app.ts index 812977c..ff53cd1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,4 +30,7 @@ async function startServer() { } } -startServer(); \ No newline at end of file +if (process.env.NODE_ENV !== "test") { + startServer(); +} + diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index c652ddc..45481cc 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -2,6 +2,14 @@ import { Sequelize } from "sequelize"; import dotenv from "dotenv"; dotenv.config(); +if (process.env.NODE_ENV === 'test') { + // Si estamos en "test", carga el archivo .env.test + dotenv.config({ path: '.env.test' }); +} else { + // Para cualquier otro caso (desarrollo, producción), carga .env + dotenv.config(); +} + const db_connection = new Sequelize( process.env.DB_NAME as string, process.env.DB_USER as string, @@ -9,10 +17,11 @@ const db_connection = new Sequelize( { host: "localhost", dialect: "mysql", + logging: process.env.NODE_ENV === 'test' ? false : console.log, define: { timestamps: false, //esta parte es un añadido por lo de createAT y updateAt }, } ); -export default db_connection; \ No newline at end of file +export default db_connection; diff --git a/test/article.test.ts b/test/article.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/setup-env.ts b/test/setup-env.ts new file mode 100644 index 0000000..01e9a4d --- /dev/null +++ b/test/setup-env.ts @@ -0,0 +1,11 @@ +// tests/setup-db.ts +import db from "../src/database/db_connection"; + +export async function setupDB() { + // Crea tablas para tests (en limpio) + await db.sync({ force: true }); +} + +export async function closeDB() { + await db.close(); +} From 130d258a2386b707ce0bda30331c1f297032c0d0 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Thu, 2 Oct 2025 14:26:38 +0200 Subject: [PATCH 21/55] feat(test): Add user auth tests and configure Jest for TS/ESM --- jest.config.js | 16 ++++ package.json | 4 +- src/app.ts | 9 +- src/database/db_connection.ts | 17 +++- test/auth.test.ts | 154 ++++++++++++++++++++++++++++++++++ tsconfig.json | 25 +++--- 6 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 jest.config.js create mode 100644 test/auth.test.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..da59596 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +// jest.config.js +export default { + transform: { + '^.+\\.ts$': 'ts-jest', + }, + testEnvironment: 'node', + // La siguiente línea es crucial para que Jest funcione con ES Modules + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + preset: 'ts-jest/presets/default-esm', + testMatch: [ + "**/test/**/*.test.ts" + ], +}; \ No newline at end of file diff --git a/package.json b/package.json index 500edf4..103af2c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "tsx src/app.ts", "build": "tsc", "start": "node dist/app.js", - "test": "jest --watchAll --no-cache" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" + }, "repository": { "type": "git", @@ -43,4 +44,5 @@ "tsx": "^4.20.6", "typescript": "^5.9.2" } + } diff --git a/src/app.ts b/src/app.ts index 812977c..14b51d0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,11 @@ import "./models/UserModel"; import "./models/ArticleModel"; import authRouter from "./routes/authRoutes.js"; import articleRouter from "./routes/articleRoutes.js"; +import { User } from "./models/UserModel.js"; +import { Article } from "./models/ArticleModel.js"; + +User.hasMany(Article, { foreignKey: 'creator_id' }); +Article.belongsTo(User, { foreignKey: 'creator_id' }); export const app = express(); const PORT = process.env.PORT || 8080; @@ -30,4 +35,6 @@ async function startServer() { } } -startServer(); \ No newline at end of file +if (process.env.NODE_ENV !== 'test') { + startServer(); +} \ No newline at end of file diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index c652ddc..9c63b35 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -1,16 +1,27 @@ +// src/database/db_connection.ts + import { Sequelize } from "sequelize"; import dotenv from "dotenv"; -dotenv.config(); + +// Si el entorno es 'test', carga las variables de .env.test +// De lo contrario, carga las de .env (comportamiento por defecto) +if (process.env.NODE_ENV === 'test') { + dotenv.config({ path: '.env.test' }); +} else { + dotenv.config(); +} const db_connection = new Sequelize( process.env.DB_NAME as string, process.env.DB_USER as string, process.env.DB_PASS as string, { - host: "localhost", + host: process.env.DB_HOST || "localhost", dialect: "mysql", + // Opcional: Desactiva los logs de SQL cuando se ejecutan los tests + logging: process.env.NODE_ENV === 'test' ? false : console.log, define: { - timestamps: false, //esta parte es un añadido por lo de createAT y updateAt + timestamps: false, }, } ); diff --git a/test/auth.test.ts b/test/auth.test.ts new file mode 100644 index 0000000..afddfc8 --- /dev/null +++ b/test/auth.test.ts @@ -0,0 +1,154 @@ +// src/__tests__/auth.test.ts + +import supertest from "supertest"; +import { app } from "../src/app.js"; // Importamos la app de express +import db_connection from "../src/database/db_connection.js"; +import UserModel from "../src/models/UserModel.js"; +import { Article } from "../src/models/ArticleModel.js"; + +// Creamos un agente de supertest para hacer peticiones +const request = supertest(app); + +// --- HOOKS DE JEST --- +// Antes de que empiecen todos los tests... +beforeAll(async () => { + // Conectamos a la base de datos de test + await db_connection.sync({ force: true }); // force: true borra y recrea las tablas +}); + +afterEach(async () => { + // Borramos en el orden correcto para respetar la clave foránea + await Article.destroy({ where: {} }); + await UserModel.destroy({ where: {} }); +}); + +// Después de que terminen todos los tests... +afterAll(async () => { + // Cerramos la conexión a la base de datos + await db_connection.close(); +}); + +// --- TESTS PARA REGISTRO --- +describe("POST /auth/register", () => { + + it("debería registrar un nuevo usuario y devolver un token", async () => { + const newUser = { + username: "testuser", + name: "Test", + last_name: "User", + email: "test@example.com", + password: "password123", + }; + + const response = await request.post("/auth/register").send(newUser); + + // 1. Comprobamos el Status Code + expect(response.status).toBe(201); + + // 2. Comprobamos que la respuesta tenga un token + expect(response.body).toHaveProperty("token"); + + // 3. Comprobamos que el usuario devuelto sea el correcto (sin la contraseña) + expect(response.body.user).toMatchObject({ + username: newUser.username, + email: newUser.email, + name: newUser.name, + last_name: newUser.last_name + }); + }); + + it("debería devolver un error 409 si el email ya existe", async () => { + // Primero creamos un usuario + const user = { + username: "testuser", + name: "Test", + last_name: "User", + email: "test@example.com", + password: "password123", + }; + await UserModel.create(user); + + // Intentamos registrarlo de nuevo con el mismo email + const response = await request.post("/auth/register").send({ + ...user, + username: "anotheruser" // cambiamos el username para que el error sea solo por el email + }); + + // Comprobamos el error + expect(response.status).toBe(409); + expect(response.body.message).toBe("El email ya está registrado"); + }); + + it("debería devolver un error 422 por datos de validación inválidos", async () => { + const invalidUser = { + username: "a", // muy corto + name: "Test", + last_name: "User", + email: "not-an-email", // email inválido + password: "123", // muy corta + }; + + const response = await request.post("/auth/register").send(invalidUser); + + expect(response.status).toBe(422); // 422 Unprocessable Entity + expect(response.body.errors).toBeInstanceOf(Array); + // Comprobamos que contiene al menos un error para el campo 'username' + expect(response.body.errors.some((err: any) => err.field === "username")).toBe(true); + }); +}); + +// --- TESTS PARA LOGIN --- +describe("POST /auth/login", () => { + + // Preparamos un usuario en la BD antes de cada test de login + beforeEach(async () => { + const user = { + username: "loginuser", + name: "Login", + last_name: "User", + email: "login@example.com", + password: "password123", // La contraseña se hasheará en el controlador + role: "user" + }; + // Simulamos el proceso de registro para tener una contraseña hasheada + await request.post("/auth/register").send(user); + }); + + it("debería loguear a un usuario existente y devolver un token", async () => { + const credentials = { + email: "login@example.com", + password: "password123", + }; + + const response = await request.post("/auth/login").send(credentials); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("token"); + expect(response.body.user.email).toBe(credentials.email); + }); + + it("debería devolver un error 401 con contraseña incorrecta", async () => { + const credentials = { + email: "login@example.com", + password: "wrongpassword", + }; + + const response = await request.post("/auth/login").send(credentials); + + expect(response.status).toBe(401); + expect(response.body.message).toBe("Contraseña incorrecta"); + }); + + it("debería devolver un error 404 si el usuario no existe", async () => { + const credentials = { + email: "nonexistent@example.com", + password: "anypassword", + }; + + const response = await request.post("/auth/login").send(credentials); + + expect(response.status).toBe(404); + expect(response.body.message).toBe("Usuario no encontrado"); + }); + +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3678a1a..61d0f9f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,28 +27,29 @@ { "compilerOptions": { /* --- Rutas --- */ - "rootDir": "./src", // Tu código .ts vive en src - "outDir": "./dist", // El JS compilado saldrá en dist + "rootDir": "./src", // Tu código .ts vive en src + "outDir": "./dist", // El JS compilado saldrá en dist /* --- Módulos/Entorno (Node ESM) --- */ - "module": "nodenext", // ESM nativo de Node - "moduleResolution": "nodenext", // Resolver imports al estilo Node ESM - "target": "ES2022", // JS moderno compatible con Node 22 - "lib": ["ES2022"], // Librerías base - "types": ["node"], // Tipos de Node (fs, process, etc.) - "resolveJsonModule": true, // Permite import de .json (opcional) + "module": "nodenext", // ESM nativo de Node + "moduleResolution": "nodenext", // Resolver imports al estilo Node ESM + "target": "ES2022", // JS moderno compatible con Node 22 + "lib": ["ES2022"], // Librerías base + "types": ["node", "jest"], + "resolveJsonModule": true, // Permite import de .json (opcional) /* --- Compatibilidad y calidad --- */ - "esModuleInterop": true, // Imports por defecto más cómodos - "skipLibCheck": true, // Acelera compilación - "strict": true, // Reglas estrictas + "esModuleInterop": true, // Imports por defecto más cómodos + "skipLibCheck": true, // Acelera compilación + "strict": true, // Reglas estrictas "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "noUnusedParameters": true, + "isolatedModules": true, /* --- Debug --- */ "sourceMap": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "test/**/*"], // <--- AÑADE "test/**/*" AQUÍ "exclude": ["node_modules"] } From 8f0159c9265cbf745dc35f3b657d8ec41c36e565 Mon Sep 17 00:00:00 2001 From: gemayc Date: Thu, 2 Oct 2025 16:04:03 +0200 Subject: [PATCH 22/55] chore(test): configure TypeScript for the testing environment --- jest.config.cjs | 9 +++++ jest.config.js | 16 --------- package-lock.json | 34 +++++++++++++++--- package.json | 7 ++-- src/database/db_connection.ts | 67 +++++++++++++++++++++++++---------- test/article.test.ts | 5 +++ test/jest.setup.ts | 21 +++++++++++ tsconfig.json | 4 +-- 8 files changed, 118 insertions(+), 45 deletions(-) create mode 100644 jest.config.cjs delete mode 100644 jest.config.js create mode 100644 test/jest.setup.ts diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..8de24c0 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,9 @@ +// jest.config.cjs + +/** Config mínima de Jest con ts-jest (sin ESM para no liarnos) */ +module.exports = { + // 1) Usamos ts-jest para transformar TypeScript a JS dentro de Jest + preset: "ts-jest", + // 2) Los tests corren en Node + testEnvironment: "node" +}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index da59596..0000000 --- a/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -// jest.config.js -export default { - transform: { - '^.+\\.ts$': 'ts-jest', - }, - testEnvironment: 'node', - // La siguiente línea es crucial para que Jest funcione con ES Modules - extensionsToTreatAsEsm: ['.ts'], - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, - preset: 'ts-jest/presets/default-esm', - testMatch: [ - "**/test/**/*.test.ts" - ], -}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a4f6cbf..b988f80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,11 +24,12 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", + "cross-env": "^10.1.0", "jest": "^30.2.0", "supertest": "^7.1.4", "ts-jest": "^29.4.4", "tsx": "^4.20.6", - "typescript": "^5.9.2" + "typescript": "^5.9.3" } }, "node_modules/@babel/code-frame": { @@ -561,6 +562,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -2775,6 +2783,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6374,9 +6400,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 8c54908..d80538b 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "dev": "tsx src/app.ts", "build": "tsc", "start": "node dist/app.js", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" - + "test": "cross-env NODE_ENV=test jest --config jest.config.cjs --runInBand" }, "repository": { "type": "git", @@ -38,11 +37,11 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.5.2", "@types/supertest": "^6.0.3", + "cross-env": "^10.1.0", "jest": "^30.2.0", "supertest": "^7.1.4", "ts-jest": "^29.4.4", "tsx": "^4.20.6", - "typescript": "^5.9.2" + "typescript": "^5.9.3" } - } diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index 59f0bd1..7346873 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -1,24 +1,56 @@ -// src/database/db_connection.ts + + +// import { Sequelize } from "sequelize"; +// import dotenv from "dotenv"; + +// // Si el entorno es 'test', carga las variables de .env.test +// // De lo contrario, carga las de .env (comportamiento por defecto) +// if (process.env.NODE_ENV === 'test') { +// dotenv.config({ path: '.env.test' }); +// } else { +// dotenv.config(); +// } + +// if (process.env.NODE_ENV === 'test') { +// // Si estamos en "test", carga el archivo .env.test +// dotenv.config({ path: '.env.test' }); +// } else { +// // Para cualquier otro caso (desarrollo, producción), carga .env +// dotenv.config(); +// } + +// const db_connection = new Sequelize( +// process.env.DB_NAME as string, +// process.env.DB_USER as string, +// process.env.DB_PASS as string, +// { +// host: process.env.DB_HOST || "localhost", +// dialect: "mysql", +// // Opcional: Desactiva los logs de SQL cuando se ejecutan los tests +// logging: process.env.NODE_ENV === 'test' ? false : console.log, +// define: { +// timestamps: false, +// }, +// } +// ); + +// export default db_connection; import { Sequelize } from "sequelize"; import dotenv from "dotenv"; -// Si el entorno es 'test', carga las variables de .env.test -// De lo contrario, carga las de .env (comportamiento por defecto) -if (process.env.NODE_ENV === 'test') { - dotenv.config({ path: '.env.test' }); -} else { - dotenv.config(); -} +// 1) Cargar .env.test si estamos en test; si no, .env normal +dotenv.config({ path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" }); + +// 2) Bandera simple para saber si estamos en test +const isTest = process.env.NODE_ENV === "test"; -if (process.env.NODE_ENV === 'test') { - // Si estamos en "test", carga el archivo .env.test - dotenv.config({ path: '.env.test' }); -} else { - // Para cualquier otro caso (desarrollo, producción), carga .env - dotenv.config(); +// 3) Seguridad: en test, exige que la BD termine en _test +if (isTest && process.env.DB_NAME && !process.env.DB_NAME.endsWith("_test")) { + throw new Error(`En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}`); } +// 4) Conexión const db_connection = new Sequelize( process.env.DB_NAME as string, process.env.DB_USER as string, @@ -26,11 +58,8 @@ const db_connection = new Sequelize( { host: process.env.DB_HOST || "localhost", dialect: "mysql", - // Opcional: Desactiva los logs de SQL cuando se ejecutan los tests - logging: process.env.NODE_ENV === 'test' ? false : console.log, - define: { - timestamps: false, - }, + logging: isTest ? false : console.log, + define: { timestamps: false }, } ); diff --git a/test/article.test.ts b/test/article.test.ts index e69de29..a050fc1 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -0,0 +1,5 @@ +describe("Article", () => { + test("debería pasar", () => { + expect(true).toBe(true); + }); +}); diff --git a/test/jest.setup.ts b/test/jest.setup.ts new file mode 100644 index 0000000..6231082 --- /dev/null +++ b/test/jest.setup.ts @@ -0,0 +1,21 @@ +process.env.NODE_ENV = "test"; + +// 2) Cargo variables de .env.test +import dotenv from "dotenv"; +dotenv.config({ path: ".env.test" }); + +// 3) Importo conexión y MODELOS (¡importar los modelos es clave!) +import db_connection from "../src/database/db_connection.js"; + + + +// 4) Antes de todo: conectar y crear tablas desde modelos +beforeAll(async () => { + await db_connection.authenticate(); + await db_connection.sync({ force: true }); // borra si hay y recrea limpio para test +}); + +// 5) Después de todo: cerrar conexión +afterAll(async () => { + await db_connection.close(); +}); diff --git a/tsconfig.json b/tsconfig.json index b666926..69d2b57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,7 @@ /* --- Debug --- */ "sourceMap": true }, - "include": ["src/**/*", "test/**/*"], // <--- AÑADE "test/**/*" AQUÍ - "exclude": ["node_modules"] + "include": ["src/**/*"], // <--- AÑADE "test/**/*" AQUÍ + "exclude": ["node_modules", "test/**/*","dist"] } From a9b01e508d69058b68628978f1365b75d9761c6b Mon Sep 17 00:00:00 2001 From: gemayc Date: Thu, 2 Oct 2025 20:46:57 +0200 Subject: [PATCH 23/55] chore(test): create test database, sync it and update Jest configuration (jest.config, jest.setup) --- jest.config.cjs | 9 --------- jest.config.mjs | 33 ++++++++++++++++++++++++++++++ package.json | 2 +- src/database/db_connection.ts | 38 ----------------------------------- test/auth.test.ts | 27 +++---------------------- 5 files changed, 37 insertions(+), 72 deletions(-) delete mode 100644 jest.config.cjs create mode 100644 jest.config.mjs diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index 8de24c0..0000000 --- a/jest.config.cjs +++ /dev/null @@ -1,9 +0,0 @@ -// jest.config.cjs - -/** Config mínima de Jest con ts-jest (sin ESM para no liarnos) */ -module.exports = { - // 1) Usamos ts-jest para transformar TypeScript a JS dentro de Jest - preset: "ts-jest", - // 2) Los tests corren en Node - testEnvironment: "node" -}; diff --git a/jest.config.mjs b/jest.config.mjs new file mode 100644 index 0000000..2ffa364 --- /dev/null +++ b/jest.config.mjs @@ -0,0 +1,33 @@ + + +// 1) Exporto en ESM porque tu proyecto es ESM (type: module / NodeNext) +export default { + // 2) Preset de ts-jest para ESM + preset: "ts-jest/presets/default-esm", + + // 3) Entorno de Node + testEnvironment: "node", + + // 4) Fuerzo a ts-jest a ESM explícito + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + tsconfig: "./tsconfig.json" + } + ] + }, + + // 5) Trata .ts como módulos ESM + extensionsToTreatAsEsm: [".ts"], + + // 6) Arreglo común: si importas con sufijo .js en código TS, + // mapea a la ruta sin .js para que Jest lo resuelva bien. + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1" + }, + setupFilesAfterEnv: ["/test/jest.setup.ts"], + // 7) Dónde están tus tests + testMatch: ["**/test/**/*.test.ts"] +}; diff --git a/package.json b/package.json index d80538b..3e2fe26 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "tsx src/app.ts", "build": "tsc", "start": "node dist/app.js", - "test": "cross-env NODE_ENV=test jest --config jest.config.cjs --runInBand" + "test": "cross-env NODE_ENV=test node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand" }, "repository": { "type": "git", diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index 7346873..a7e84e9 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -1,41 +1,3 @@ - - -// import { Sequelize } from "sequelize"; -// import dotenv from "dotenv"; - -// // Si el entorno es 'test', carga las variables de .env.test -// // De lo contrario, carga las de .env (comportamiento por defecto) -// if (process.env.NODE_ENV === 'test') { -// dotenv.config({ path: '.env.test' }); -// } else { -// dotenv.config(); -// } - -// if (process.env.NODE_ENV === 'test') { -// // Si estamos en "test", carga el archivo .env.test -// dotenv.config({ path: '.env.test' }); -// } else { -// // Para cualquier otro caso (desarrollo, producción), carga .env -// dotenv.config(); -// } - -// const db_connection = new Sequelize( -// process.env.DB_NAME as string, -// process.env.DB_USER as string, -// process.env.DB_PASS as string, -// { -// host: process.env.DB_HOST || "localhost", -// dialect: "mysql", -// // Opcional: Desactiva los logs de SQL cuando se ejecutan los tests -// logging: process.env.NODE_ENV === 'test' ? false : console.log, -// define: { -// timestamps: false, -// }, -// } -// ); - -// export default db_connection; - import { Sequelize } from "sequelize"; import dotenv from "dotenv"; diff --git a/test/auth.test.ts b/test/auth.test.ts index afddfc8..daac26d 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -2,32 +2,10 @@ import supertest from "supertest"; import { app } from "../src/app.js"; // Importamos la app de express -import db_connection from "../src/database/db_connection.js"; -import UserModel from "../src/models/UserModel.js"; -import { Article } from "../src/models/ArticleModel.js"; + // Creamos un agente de supertest para hacer peticiones const request = supertest(app); - -// --- HOOKS DE JEST --- -// Antes de que empiecen todos los tests... -beforeAll(async () => { - // Conectamos a la base de datos de test - await db_connection.sync({ force: true }); // force: true borra y recrea las tablas -}); - -afterEach(async () => { - // Borramos en el orden correcto para respetar la clave foránea - await Article.destroy({ where: {} }); - await UserModel.destroy({ where: {} }); -}); - -// Después de que terminen todos los tests... -afterAll(async () => { - // Cerramos la conexión a la base de datos - await db_connection.close(); -}); - // --- TESTS PARA REGISTRO --- describe("POST /auth/register", () => { @@ -66,7 +44,8 @@ describe("POST /auth/register", () => { email: "test@example.com", password: "password123", }; - await UserModel.create(user); + // await UserModel.create(user); + await request.post("/auth/register").send(user); // Intentamos registrarlo de nuevo con el mismo email const response = await request.post("/auth/register").send({ From 8bbe666cf519190a1f07d6fdbb876865a9a4b079 Mon Sep 17 00:00:00 2001 From: gemayc Date: Thu, 2 Oct 2025 23:24:16 +0200 Subject: [PATCH 24/55] fix(articles): validate requests before controllers to prevent 500 errors --- src/models/ArticleModel.ts | 2 +- src/routes/articleRoutes.ts | 8 ++++---- test/jest.setup.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts index 3bbb455..a4103cf 100644 --- a/src/models/ArticleModel.ts +++ b/src/models/ArticleModel.ts @@ -75,7 +75,7 @@ Article.init( allowNull: false, validate: { notNull: { msg: "content no puede estar vacío" }, - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + len: { args: [6, 65535], msg: "content mínimo 6 caracteres" }, }, }, category: { diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 9919563..3eeb121 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -10,12 +10,12 @@ const articleRouter = express.Router(); // Rutas públicas (sin autenticación) articleRouter.get("/", getAllArticles); -articleRouter.get("/:id", getArticleById, idParamValidators,checkValidations); +articleRouter.get("/:id", idParamValidators, checkValidations, getArticleById,); // Rutas protegidas (handleValidation, createArticle, createArticleValidators, checkValidations); -articleRouter.post("/", authMiddleware, requireRole("admin"), createArticle, handleValidation, createArticleValidators,); -articleRouter.put("/:id", authMiddleware, requireRole("admin"), handleValidation, updateArticle, updateArticleValidators, checkValidations); -articleRouter.delete("/:id",authMiddleware, requireRole("admin"), handleValidation, deleteArticle); +articleRouter.post("/", authMiddleware, requireRole("admin"), createArticleValidators, handleValidation, createArticle,); +articleRouter.put("/:id", authMiddleware, requireRole("admin"), idParamValidators, updateArticleValidators, handleValidation, checkValidations, updateArticle,); +articleRouter.delete("/:id",authMiddleware, requireRole("admin"), idParamValidators, handleValidation, deleteArticle); export default articleRouter; \ No newline at end of file diff --git a/test/jest.setup.ts b/test/jest.setup.ts index 6231082..5051582 100644 --- a/test/jest.setup.ts +++ b/test/jest.setup.ts @@ -1,21 +1,21 @@ process.env.NODE_ENV = "test"; -// 2) Cargo variables de .env.test +// Cargo variables de .env.test import dotenv from "dotenv"; dotenv.config({ path: ".env.test" }); -// 3) Importo conexión y MODELOS (¡importar los modelos es clave!) +// Importo conexión y MODELOS (¡importar los modelos es clave!) import db_connection from "../src/database/db_connection.js"; -// 4) Antes de todo: conectar y crear tablas desde modelos +// Antes de todo: conectar y crear tablas desde modelos beforeAll(async () => { await db_connection.authenticate(); await db_connection.sync({ force: true }); // borra si hay y recrea limpio para test }); -// 5) Después de todo: cerrar conexión +// Después de todo: cerrar conexión afterAll(async () => { await db_connection.close(); }); From 416224f888de01cf1f251e3f9fcd5bed32eb4757 Mon Sep 17 00:00:00 2001 From: gemayc Date: Fri, 3 Oct 2025 21:39:49 +0200 Subject: [PATCH 25/55] test: implement POST request tests for article creation and validation --- src/controllers/ArticleController.ts | 16 +++- src/middlewares/articleMiddlewares.ts | 1 + test/article.test.ts | 127 +++++++++++++++++++++++++- test/auth.test.ts | 2 - 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index 950184f..abad758 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -1,5 +1,6 @@ import { Article } from "../models/ArticleModel.js"; -import type { Request, Response } from "express"; +import type { Request, Response} from "express"; +import User from "../models/UserModel.js"; export const getAllArticles = async (_req: Request, res: Response) => { try { @@ -48,6 +49,16 @@ export const createArticle = async (req: Request, res: Response) => { try { // Aquí filtramos los campos que sí queremos guardar const {title, description, content, category, species, image, references, creator_id, } = req.body; + // Verifica si algún campo esencial falta + if (!title || !description || !content || !category || !species || !creator_id) { + console.error("Faltan datos necesarios para crear el artículo"); + return res.status(400).json({ message: "Faltan datos necesarios" }); + } + // Verifica si el creator_id existe en la base de datos + const user = await User.findByPk(creator_id); // Busca al usuario por su ID + if (!user) { + return res.status(400).json({ message: "El creador del artículo no existe." }); + } // Creamos el artículo solo con esos campos (los demás se ignoran) const newArticle = await Article.create({ @@ -59,6 +70,9 @@ export const createArticle = async (req: Request, res: Response) => { image, references, creator_id, + }).catch((error) => { + console.error("Error en la base de datos:", error); + throw new Error("Simulación de error en la base de datos"); }); return res.status(201).json(newArticle); diff --git a/src/middlewares/articleMiddlewares.ts b/src/middlewares/articleMiddlewares.ts index 8d23703..5952538 100644 --- a/src/middlewares/articleMiddlewares.ts +++ b/src/middlewares/articleMiddlewares.ts @@ -23,3 +23,4 @@ export function checkValidations(req: Request, res: Response, next: NextFunction // 6) Si no hay errores, seguimos al siguiente middleware/controlador next(); } + diff --git a/test/article.test.ts b/test/article.test.ts index a050fc1..27acc60 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -1,5 +1,124 @@ -describe("Article", () => { - test("debería pasar", () => { - expect(true).toBe(true); - }); +import supertest from "supertest"; +import { app } from "../src/app.js"; + +//Creamos el "agente" de supertest para enviar requests a la app. +const request = supertest(app); + +//Aquí guardaremos el token del admin que creemos en beforeAll. +let adminToken: string; + +// 5) Un helper pequeñito para crear payloads de artículo sin repetir mucho. +const makeArticle = (overrides: Partial> = {}) => ({ + title: "Nuevo artículo", + description: "Descripción válida", + content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", + category: "General", + species: "Animal", + image: "https://imagen.com", + references: "https://referencia.com", + creator_id: "admin_id", + ...overrides, +}); + + +describe("Rutas de Artículos", () => { + // Antes de todos los tests: + // Registramos un usuario admin + // Guardamos su token para las rutas protegidas (POST/PUT/DELETE). + + let adminId: string; // Para almacenar el ID del usuario administrador + + beforeAll(async () => { + const adminUser = { + username: "adminuser", + name: "Admin", + last_name: "User", + email: "admin.articles@example.com", + password: "password123", + role: "admin", + }; + + // Registramos al admin + const res = await request.post("/auth/register").send(adminUser); + + // Verificamos la respuesta y el token + console.log("Admin User Registered:", res.body); + + + if (res.body.token) { + adminToken = res.body.token; + adminId = res.body.user.id; // Guardamos el ID del administrador + console.log("Admin Token:", adminToken); + } else { + console.log("Error: No token found in response."); + } }); + + // Variable para guardar el id del artículo que creemos. + let createdId: string; + + + // TEST: crear artículo (POST /articles). + it("POST /articles — crea un artículo (201) y lo devuelve", async () => { + // Preparamos el body del artículo. + const body = makeArticle({ + creator_id: adminId, // Asigna el ID del admin + }); + console.log("Cuerpo del artículo:", body); // Verifica los datos enviados + console.log("Longitud de `content`:", body.content.length); // Verifica la longitud de content + + // Enviamos la petición con el token de admin. + const res = await request + .post("/article") + .set("Authorization", `Bearer ${adminToken}`) + .send(body); + + console.log("Response body:", res.body); // Verifica la respuesta del servidor + // Comprobamos status y propiedades básicas. + expect(res.status).toBe(201); + expect(res.body).toHaveProperty("id"); // Sequelize debería volver con ID + expect(res.body).toMatchObject({ + title: body.title, + description: body.description, + content: body.content, + category: body.category, + species: body.species, + image: body.image, + references: body.references, + creator_id: body.creator_id, + }); + + // Guardamos el id para los siguientes tests. + createdId = String(res.body.id); + }); +// it("POST /articles - debe devolver un error 500 si no se puede crear el artículo", async () => { + +// const body = { +// title: "Nuevo artículo", +// description: "Descripción válida", +// content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", +// category: "General", +// image: "https://imagen.com", +// species: "Animal", +// creator_id: "admin_id" + +// }; + +// // Enviamos la solicitud con el token de admin +// const res = await request +// .post("/article") +// .set("Authorization", `Bearer ${adminToken}`) +// .send(body); + +// // Imprime la respuesta para ver el error +// console.log("Response body:", res.body); + +// // Verifica que la respuesta tenga el status 400 y el mensaje adecuado +// expect(res.status).toBe(500); +// expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); +// }); + + + + +}); \ No newline at end of file diff --git a/test/auth.test.ts b/test/auth.test.ts index daac26d..7e1c413 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -1,5 +1,3 @@ -// src/__tests__/auth.test.ts - import supertest from "supertest"; import { app } from "../src/app.js"; // Importamos la app de express From 445d0e2b4c54dc1b9051b4cd22935d3a48763b3f Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 6 Oct 2025 09:32:36 +0200 Subject: [PATCH 26/55] feat(auth): update AuthController logic and error handling --- .../{auth.ts => authMiddlewares.ts} | 0 src/routes/articleRoutes.ts | 2 +- src/routes/authRoutes.ts | 2 +- test/.gitkeep | 0 test/article.test.ts | 24 ++++++++++++++++++- 5 files changed, 25 insertions(+), 3 deletions(-) rename src/middlewares/{auth.ts => authMiddlewares.ts} (100%) delete mode 100644 test/.gitkeep diff --git a/src/middlewares/auth.ts b/src/middlewares/authMiddlewares.ts similarity index 100% rename from src/middlewares/auth.ts rename to src/middlewares/authMiddlewares.ts diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 3eeb121..0781f3d 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -1,6 +1,6 @@ import express from 'express'; import {getAllArticles,getArticleById, deleteArticle,createArticle,updateArticle} from '../controllers/ArticleController.js'; -import { authMiddleware, requireRole, handleValidation} from '../middlewares/auth.js'; +import { authMiddleware, requireRole, handleValidation} from '../middlewares/authMiddlewares.js'; import { createArticleValidators, updateArticleValidators, idParamValidators } from '../validators/articleValidators.js'; import { checkValidations } from "../middlewares/articleMiddlewares.js"; diff --git a/src/routes/authRoutes.ts b/src/routes/authRoutes.ts index 0ac51a3..ea9747e 100644 --- a/src/routes/authRoutes.ts +++ b/src/routes/authRoutes.ts @@ -1,7 +1,7 @@ import express from "express"; import { registerController, loginController } from "../controllers/AuthController.js"; import { registerValidator, loginValidator } from "../validators/userValidators.js"; -import { handleValidation } from "../middlewares/auth.js"; +import { handleValidation } from "../middlewares/authMiddlewares.js"; const authRouter = express.Router(); diff --git a/test/.gitkeep b/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/article.test.ts b/test/article.test.ts index 27acc60..68b9708 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -91,6 +91,7 @@ describe("Rutas de Artículos", () => { // Guardamos el id para los siguientes tests. createdId = String(res.body.id); }); + // it("POST /articles - debe devolver un error 500 si no se puede crear el artículo", async () => { // const body = { @@ -118,7 +119,28 @@ describe("Rutas de Artículos", () => { // expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); // }); + // it("POST /article - error 500 si falla la base de datos", async () => { + // // Mock para forzar fallo en Article.create + // jest.spyOn(Article, "create").mockImplementation(() => { + // throw new Error("Fallo de base de datos simulado"); + // }); + + // const body = makeArticle(); + // const res = await request + // .post("/article") + // .set("Authorization", `Bearer ${adminToken}`) + // .send(body); + + // expect(res.status).toBe(500); + // expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); + + // // Restaurar implementación original + // (Article.create as jest.Mock).mockRestore(); + // }); + + -}); \ No newline at end of file + }); + From bc335360a0e4962c1c59ea99b0e17b176d58ca52 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Mon, 6 Oct 2025 09:36:29 +0200 Subject: [PATCH 27/55] feature: Add forgot password feature --- package-lock.json | 8 ++ package.json | 1 + src/app.ts | 7 ++ src/controllers/PasswordResetController.ts | 92 ++++++++++++++++++++++ src/middlewares/handleValidation.ts | 24 ++++++ src/models/PasswordResetToken.ts | 41 ++++++++++ src/routes/passwordReset.routes.ts | 11 +++ src/utils/resetToken.ts | 9 +++ src/validators/passwordResetValidators.ts | 16 ++++ 9 files changed, 209 insertions(+) create mode 100644 src/controllers/PasswordResetController.ts create mode 100644 src/middlewares/handleValidation.ts create mode 100644 src/models/PasswordResetToken.ts create mode 100644 src/routes/passwordReset.routes.ts create mode 100644 src/utils/resetToken.ts create mode 100644 src/validators/passwordResetValidators.ts diff --git a/package-lock.json b/package-lock.json index b988f80..98c770a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "bcryptjs": "^3.0.2", + "crypto": "^1.0.1", "dotenv": "^17.2.2", "express": "^5.1.0", "express-validator": "^7.2.1", @@ -2816,6 +2817,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index 3e2fe26..adaadf0 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "description": "", "dependencies": { "bcryptjs": "^3.0.2", + "crypto": "^1.0.1", "dotenv": "^17.2.2", "express": "^5.1.0", "express-validator": "^7.2.1", diff --git a/src/app.ts b/src/app.ts index b270237..33ef699 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,6 +7,9 @@ import authRouter from "./routes/authRoutes.js"; import articleRouter from "./routes/articleRoutes.js"; import { User } from "./models/UserModel.js"; import { Article } from "./models/ArticleModel.js"; +import passwordResetRouter from "./routes/passwordReset.routes.js"; +import "./models/PasswordResetToken.js"; + User.hasMany(Article, { foreignKey: 'creator_id' }); Article.belongsTo(User, { foreignKey: 'creator_id' }); @@ -21,6 +24,10 @@ Article.belongsTo(User, { foreignKey: 'creator_id' }); app.use("/auth", authRouter ) app.use("/article", articleRouter) +app.use("/auth", passwordResetRouter); + +await db_connection.sync({ alter: true }); // o { force: true } si quieres regenerar + async function startServer() { try { // Sincroniza los modelos con la base de datos diff --git a/src/controllers/PasswordResetController.ts b/src/controllers/PasswordResetController.ts new file mode 100644 index 0000000..9639a7c --- /dev/null +++ b/src/controllers/PasswordResetController.ts @@ -0,0 +1,92 @@ +import { Request, Response } from "express"; +import bcrypt from "bcryptjs"; +import User from "../models/UserModel.js"; +import PasswordResetToken from "../models/PasswordResetToken.js"; +import { generateRawToken, hashToken } from "../utils/resetToken.js"; + +// Enviar email (usa nodemailer en prod; aquí vamos a loguear el link en dev) +async function sendResetEmail(to: string, url: string) { + if (process.env.NODE_ENV === "production") { + // TODO: integra nodemailer o tu proveedor (Sendgrid, SES, etc.) + } else { + console.log("🔗 Reset link (dev):", url); + } +} + +export const forgotPassword = async (req: Request, res: Response): Promise => { + const normalizedEmail = String(req.body.email).toLowerCase().trim(); + + // Respuesta genérica SIEMPRE (no revelar si existe) + const generic = { message: "Si el email existe, te enviaremos instrucciones para restablecer tu contraseña." }; + + // Busca usuario + const user = await User.findOne({ where: { email: normalizedEmail } }); + if (!user) { + res.status(200).json(generic); + return; + } + + // Genera token + const rawToken = generateRawToken(); + const tokenHash = hashToken(rawToken); + const expires = new Date(Date.now() + 15 * 60 * 1000); // 15 min + + // Opcional: invalida tokens previos no usados + await PasswordResetToken.update( + { used_at: new Date() }, + { where: { user_id: user.id, used_at: null } } + ); + + // Guarda token + await PasswordResetToken.create({ + user_id: user.id, + token_hash: tokenHash, + expires_at: expires, + }); + + // Construye URL de reset + const resetUrl = `${process.env.FRONTEND_URL ?? "http://localhost:5173"}/reset-password?token=${rawToken}`; + + // Manda email (o log en dev) + await sendResetEmail(user.email, resetUrl); + + res.status(200).json(generic); +}; + +export const resetPassword = async (req: Request, res: Response): Promise => { + const { token, newPassword } = req.body as { token: string; newPassword: string }; + + const tokenHash = hashToken(token); + const now = new Date(); + + // Busca token válido + const prt = await PasswordResetToken.findOne({ + where: { + token_hash: tokenHash, + used_at: null, + }, + }); + + if (!prt || prt.expires_at < now) { + res.status(400).json({ message: "Token inválido o expirado" }); + return; + } + + // Busca usuario + const user = await User.findByPk(prt.user_id); + if (!user) { + res.status(400).json({ message: "Token inválido" }); + return; + } + + // Actualiza contraseña + const hash = await bcrypt.hash(newPassword, 10); + user.password = hash; + await user.save(); + + // Marca token como usado + prt.used_at = new Date(); + await prt.save(); + + res.status(200).json({ message: "Contraseña restablecida correctamente. Ya puedes iniciar sesión." }); +}; diff --git a/src/middlewares/handleValidation.ts b/src/middlewares/handleValidation.ts new file mode 100644 index 0000000..c9ef378 --- /dev/null +++ b/src/middlewares/handleValidation.ts @@ -0,0 +1,24 @@ +import { Request, Response, NextFunction } from "express"; +import { validationResult } from "express-validator"; + +/** + * Middleware que revisa si hay errores de validación y los devuelve en formato 422. + */ +export function handleValidation(req: Request, res: Response, next: NextFunction) { + const result = validationResult(req); + + if (!result.isEmpty()) { + // En express-validator v7 la propiedad es "path" (antes era "param") + const errors = result.array({ onlyFirstError: true }).map((err: any) => ({ + field: err.path ?? err.param ?? "unknown", + msg: err.msg, + })); + + return res.status(422).json({ + message: "Errores de validación", + errors, + }); + } + + next(); +} diff --git a/src/models/PasswordResetToken.ts b/src/models/PasswordResetToken.ts new file mode 100644 index 0000000..f9211a4 --- /dev/null +++ b/src/models/PasswordResetToken.ts @@ -0,0 +1,41 @@ +import { DataTypes, Model, Optional } from "sequelize"; +import db from "../database/db_connection.js"; + +interface PasswordResetTokenAttrs { + id: number; + user_id: bigint; // FK a users.id + token_hash: string; // SHA-256 del token + expires_at: Date; + used_at: Date | null; + created_at: Date; + updated_at: Date; +} + +type Creation = Optional< + PasswordResetTokenAttrs, + "id" | "used_at" | "created_at" | "updated_at" +>; + +export class PasswordResetToken extends Model {} + +PasswordResetToken.init( + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + user_id: { type: DataTypes.BIGINT, allowNull: false }, + token_hash: { type: DataTypes.STRING, allowNull: false }, + expires_at: { type: DataTypes.DATE, allowNull: false }, + used_at: { type: DataTypes.DATE, allowNull: true }, + created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, + updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW }, + }, + { + sequelize: db, + tableName: "password_reset_tokens", + underscored: true, + timestamps: true, + createdAt: "created_at", + updatedAt: "updated_at", + } +); + +export default PasswordResetToken; diff --git a/src/routes/passwordReset.routes.ts b/src/routes/passwordReset.routes.ts new file mode 100644 index 0000000..4270805 --- /dev/null +++ b/src/routes/passwordReset.routes.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { forgotPassword, resetPassword } from "../controllers/PasswordResetController.js"; +import { forgotValidator, resetValidator } from "../validators/passwordResetValidators.js"; +import { handleValidation } from "../middlewares/handleValidation.js"; + +const router = Router(); + +router.post("/forgot-password", forgotValidator, handleValidation, forgotPassword); +router.post("/reset-password", resetValidator, handleValidation, resetPassword); + +export default router; diff --git a/src/utils/resetToken.ts b/src/utils/resetToken.ts new file mode 100644 index 0000000..cdccd7b --- /dev/null +++ b/src/utils/resetToken.ts @@ -0,0 +1,9 @@ +import crypto from "crypto"; + +export function generateRawToken(): string { + return crypto.randomBytes(32).toString("hex"); // 64 chars +} + +export function hashToken(token: string): string { + return crypto.createHash("sha256").update(token).digest("hex"); +} diff --git a/src/validators/passwordResetValidators.ts b/src/validators/passwordResetValidators.ts new file mode 100644 index 0000000..0cf565a --- /dev/null +++ b/src/validators/passwordResetValidators.ts @@ -0,0 +1,16 @@ +import { body } from "express-validator"; + +export const forgotValidator = [ + body("email").trim().toLowerCase().isEmail().withMessage("Email inválido"), +]; + +export const resetValidator = [ + body("token").isString().isLength({ min: 10 }).withMessage("Token inválido"), + body("newPassword") + .isString() + .isLength({ min: 8 }).withMessage("La contraseña debe tener mínimo 8 caracteres") + .matches(/[a-z]/).withMessage("Debe incluir minúscula") + .matches(/[A-Z]/).withMessage("Debe incluir mayúscula") + .matches(/\d/).withMessage("Debe incluir número") + .matches(/[^\w\s]/).withMessage("Debe incluir símbolo"), +]; From 414bf533ad23da24685b110a2a3d9920f6d0b140 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Mon, 6 Oct 2025 14:01:10 +0200 Subject: [PATCH 28/55] fix: return only token when login and register --- src/controllers/AuthController.ts | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index bc08c94..f7155c1 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -81,14 +81,14 @@ export const registerController = async ( res.status(201).json({ message: "Usuario registrado exitosamente", token, - user: { - id: newUser.id.toString(), - username: newUser.username, - email: newUser.email, - name: newUser.name, - last_name: newUser.last_name, - role: newUser.role, - }, + // user: { + // id: newUser.id.toString(), + // username: newUser.username, + // email: newUser.email, + // name: newUser.name, + // last_name: newUser.last_name, + // role: newUser.role, + // }, }); } catch (error) { res.status(500).json({ @@ -141,14 +141,14 @@ export const loginController = async ( res.status(200).json({ message: "Login exitoso", token, - user: { - id: user.id.toString(), - username: user.username, - email: user.email, - name: user.name, - last_name: user.last_name, - role: user.role, - }, + // user: { + // id: user.id.toString(), + // username: user.username, + // email: user.email, + // name: user.name, + // last_name: user.last_name, + // role: user.role, + // }, }); } catch (error) { res.status(500).json({ From 6d1b282b5dd7dc3dea8cf55081b42c2477287ba4 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 6 Oct 2025 15:02:10 +0200 Subject: [PATCH 29/55] feat(auth, article): modify AuthController and ArticleController logic and functionality --- src/app.ts | 1 + src/controllers/ArticleController.ts | 29 +++++++----- src/controllers/AuthController.ts | 32 ++++++------- test/article.test.ts | 70 ++++++++++------------------ 4 files changed, 59 insertions(+), 73 deletions(-) diff --git a/src/app.ts b/src/app.ts index b270237..89b11a8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,3 +1,4 @@ +(BigInt.prototype as any).toJSON = function () { return this.toString(); }; import express from "express"; import db_connection from "../src/database/db_connection.js"; import "dotenv/config"; diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index abad758..e95a815 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -47,21 +47,25 @@ export const deleteArticle = async (req: Request, res: Response) => { export const createArticle = async (req: Request, res: Response) => { try { + // const user = await User.findByPk(creator_id); // Busca al usuario por su ID + if (!req.user) { + return res.status(400).json({ message: "El creador del artículo no existe." }); + } + // Aquí filtramos los campos que sí queremos guardar - const {title, description, content, category, species, image, references, creator_id, } = req.body; + const {title, description, content, category, species, image, references, } = req.body; // Verifica si algún campo esencial falta - if (!title || !description || !content || !category || !species || !creator_id) { + const creator_id = req.user.userId + + if (!title || !description || !content || !category || !species ) { console.error("Faltan datos necesarios para crear el artículo"); return res.status(400).json({ message: "Faltan datos necesarios" }); } - // Verifica si el creator_id existe en la base de datos - const user = await User.findByPk(creator_id); // Busca al usuario por su ID - if (!user) { - return res.status(400).json({ message: "El creador del artículo no existe." }); - } - + + // Creamos el artículo solo con esos campos (los demás se ignoran) - const newArticle = await Article.create({ + + const newArticle = await Article.create({ title, description, content, @@ -69,15 +73,16 @@ export const createArticle = async (req: Request, res: Response) => { species, image, references, - creator_id, - }).catch((error) => { + creator_id +}).catch((error) => { console.error("Error en la base de datos:", error); throw new Error("Simulación de error en la base de datos"); }); return res.status(201).json(newArticle); } catch (error) { - return res.status(500).json({ message: "No se pudo crear el artículo" }); + console.error("Error en la base de datos:", error); + return res.status(500).json({ message: "No se pudo crear el artículo", error }); } }; diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index bc08c94..f7155c1 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -81,14 +81,14 @@ export const registerController = async ( res.status(201).json({ message: "Usuario registrado exitosamente", token, - user: { - id: newUser.id.toString(), - username: newUser.username, - email: newUser.email, - name: newUser.name, - last_name: newUser.last_name, - role: newUser.role, - }, + // user: { + // id: newUser.id.toString(), + // username: newUser.username, + // email: newUser.email, + // name: newUser.name, + // last_name: newUser.last_name, + // role: newUser.role, + // }, }); } catch (error) { res.status(500).json({ @@ -141,14 +141,14 @@ export const loginController = async ( res.status(200).json({ message: "Login exitoso", token, - user: { - id: user.id.toString(), - username: user.username, - email: user.email, - name: user.name, - last_name: user.last_name, - role: user.role, - }, + // user: { + // id: user.id.toString(), + // username: user.username, + // email: user.email, + // name: user.name, + // last_name: user.last_name, + // role: user.role, + // }, }); } catch (error) { res.status(500).json({ diff --git a/test/article.test.ts b/test/article.test.ts index 68b9708..8f7d497 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -1,5 +1,6 @@ import supertest from "supertest"; import { app } from "../src/app.js"; +import { Article } from "../src/models/ArticleModel.js" //Creamos el "agente" de supertest para enviar requests a la app. const request = supertest(app); @@ -91,52 +92,31 @@ describe("Rutas de Artículos", () => { // Guardamos el id para los siguientes tests. createdId = String(res.body.id); }); + // TEST: POST /article - error 500 +it("POST /article — debe devolver un error 500 si falla la creación", async () => { + // Preparamos un body válido (igual que tu test exitoso) + const body = makeArticle({ + creator_id: adminId, + title: "FORCE_ERROR", // solo para identificar este caso si quieres + }); -// it("POST /articles - debe devolver un error 500 si no se puede crear el artículo", async () => { - -// const body = { -// title: "Nuevo artículo", -// description: "Descripción válida", -// content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", -// category: "General", -// image: "https://imagen.com", -// species: "Animal", -// creator_id: "admin_id" - -// }; - -// // Enviamos la solicitud con el token de admin -// const res = await request -// .post("/article") -// .set("Authorization", `Bearer ${adminToken}`) -// .send(body); - -// // Imprime la respuesta para ver el error -// console.log("Response body:", res.body); - -// // Verifica que la respuesta tenga el status 400 y el mensaje adecuado -// expect(res.status).toBe(500); -// expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); -// }); - - // it("POST /article - error 500 si falla la base de datos", async () => { - // // Mock para forzar fallo en Article.create - // jest.spyOn(Article, "create").mockImplementation(() => { - // throw new Error("Fallo de base de datos simulado"); - // }); - - // const body = makeArticle(); - // const res = await request - // .post("/article") - // .set("Authorization", `Bearer ${adminToken}`) - // .send(body); - - // expect(res.status).toBe(500); - // expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); - - // // Restaurar implementación original - // (Article.create as jest.Mock).mockRestore(); - // }); + // Forzamos que Article.create lance un error temporalmente + const originalCreate = Article.create; + Article.create = async () => { + throw new Error("Error forzado para test 500"); + }; + + const res = await request + .post("/article") + .set("Authorization", `Bearer ${adminToken}`) + .send(body); + + expect(res.status).toBe(500); + expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); + + // Restauramos la función original para que no afecte otros tests + Article.create = originalCreate; +}); From 94307ed970d509116000ae86167e337177089ce3 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Mon, 6 Oct 2025 15:07:46 +0200 Subject: [PATCH 30/55] Fix tests issues --- test/auth.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/auth.test.ts b/test/auth.test.ts index 7e1c413..7d15910 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -25,12 +25,12 @@ describe("POST /auth/register", () => { expect(response.body).toHaveProperty("token"); // 3. Comprobamos que el usuario devuelto sea el correcto (sin la contraseña) - expect(response.body.user).toMatchObject({ - username: newUser.username, - email: newUser.email, - name: newUser.name, - last_name: newUser.last_name - }); + // expect(response.body.user).toMatchObject({ + // username: newUser.username, + // email: newUser.email, + // name: newUser.name, + // last_name: newUser.last_name + // }); }); it("debería devolver un error 409 si el email ya existe", async () => { @@ -101,7 +101,7 @@ describe("POST /auth/login", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("token"); - expect(response.body.user.email).toBe(credentials.email); + // expect(response.body.user.email).toBe(credentials.email); }); it("debería devolver un error 401 con contraseña incorrecta", async () => { From 717e1ebc507295353179cf9a41ac2f450e6b2099 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Mon, 6 Oct 2025 15:16:45 +0200 Subject: [PATCH 31/55] Update test requirements --- src/controllers/AuthController.ts | 13 +++---------- test/auth.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index f7155c1..7152d3e 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -118,16 +118,9 @@ export const loginController = async ( where: { email: normalizedEmail } }); - if (!user) { - res.status(404).json({ message: "Usuario no encontrado" }); - return; - } - - // comparar contraseña ingresada con la hasheada en BD - const ok = await bcrypt.compare(password, user.password); - - if (!ok) { - res.status(401).json({ message: "Contraseña incorrecta" }); +if (!user || !(await bcrypt.compare(password, user.password))) { + // Devolvemos 401 con un mensaje genérico + res.status(401).json({ message: "Email o contraseña incorrectos" }); return; } diff --git a/test/auth.test.ts b/test/auth.test.ts index 7d15910..8a08a78 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -113,7 +113,7 @@ describe("POST /auth/login", () => { const response = await request.post("/auth/login").send(credentials); expect(response.status).toBe(401); - expect(response.body.message).toBe("Contraseña incorrecta"); + expect(response.body.message).toBe("Email o contraseña incorrectos"); }); it("debería devolver un error 404 si el usuario no existe", async () => { @@ -124,8 +124,8 @@ describe("POST /auth/login", () => { const response = await request.post("/auth/login").send(credentials); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Usuario no encontrado"); + expect(response.status).toBe(401); + expect(response.body.message).toBe("Email o contraseña incorrectos"); }); }); \ No newline at end of file From 523962daace9b61375164193985c7f63d2fd5c05 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 6 Oct 2025 22:25:09 +0200 Subject: [PATCH 32/55] fix(test): update error handling in article creation test --- src/app.ts | 1 - src/controllers/ArticleController.ts | 26 +++- src/middlewares/articleMiddlewares.ts | 42 +++--- src/middlewares/authMiddlewares.ts | 2 +- src/routes/articleRoutes.ts | 6 +- test/article.test.ts | 204 ++++++++++++++------------ test/auth.test.ts | 16 +- 7 files changed, 166 insertions(+), 131 deletions(-) diff --git a/src/app.ts b/src/app.ts index 89b11a8..b270237 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,3 @@ -(BigInt.prototype as any).toJSON = function () { return this.toString(); }; import express from "express"; import db_connection from "../src/database/db_connection.js"; import "dotenv/config"; diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index e95a815..c989752 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -1,6 +1,6 @@ import { Article } from "../models/ArticleModel.js"; import type { Request, Response} from "express"; -import User from "../models/UserModel.js"; + export const getAllArticles = async (_req: Request, res: Response) => { try { @@ -47,15 +47,17 @@ export const deleteArticle = async (req: Request, res: Response) => { export const createArticle = async (req: Request, res: Response) => { try { - // const user = await User.findByPk(creator_id); // Busca al usuario por su ID - if (!req.user) { - return res.status(400).json({ message: "El creador del artículo no existe." }); + // const user = await User.findByPk(creator_id); // Busca al usuario por su ID + if (!req.user || !req.user.userId) { + return res.status(400).json({ message: "El creador del artículo no está autenticado." }); } // Aquí filtramos los campos que sí queremos guardar const {title, description, content, category, species, image, references, } = req.body; // Verifica si algún campo esencial falta - const creator_id = req.user.userId + const creator_id = BigInt(req.user.userId).toString(); // Convierte BigInt a string si es necesario + + console.log("Creador ID:", creator_id); // Verifica que el creator_id sea correcto if (!title || !description || !content || !category || !species ) { console.error("Faltan datos necesarios para crear el artículo"); @@ -82,7 +84,18 @@ export const createArticle = async (req: Request, res: Response) => { return res.status(201).json(newArticle); } catch (error) { console.error("Error en la base de datos:", error); - return res.status(500).json({ message: "No se pudo crear el artículo", error }); + if (error instanceof Error) { + return res.status(500).json({ + message: "No se pudo crear el artículo", + error: error.message // Ahora TypeScript sabe que 'error' tiene la propiedad 'message' + }); + } else { + // Si el error no es una instancia de Error, enviamos un mensaje genérico + return res.status(500).json({ + message: "No se pudo crear el artículo", + error: "Error desconocido" + }); + } } }; @@ -96,6 +109,7 @@ interface UpdateArticleDTO { image?: string; references?: string; } + export const updateArticle = async ( req: Request<{ id: string }, unknown, UpdateArticleDTO>, res: Response diff --git a/src/middlewares/articleMiddlewares.ts b/src/middlewares/articleMiddlewares.ts index 5952538..e5a517e 100644 --- a/src/middlewares/articleMiddlewares.ts +++ b/src/middlewares/articleMiddlewares.ts @@ -1,26 +1,26 @@ // 1) Importo lo necesario de express y express-validator -import { validationResult } from "express-validator"; -import type { Request, Response, NextFunction } from "express"; +// import { validationResult } from "express-validator"; +// import type { Request, Response, NextFunction } from "express"; -// 2) Middleware que revisa si hubo errores de validación -export function checkValidations(req: Request, res: Response, next: NextFunction) { - // 3) Recoge el resultado de todos los body()/param()/query() anteriores - const errors = validationResult(req); +// // 2) Middleware que revisa si hubo errores de validación +// export function checkValidations(req: Request, res: Response, next: NextFunction) { +// // 3) Recoge el resultado de todos los body()/param()/query() anteriores +// const errors = validationResult(req); - // 4) Si hay errores, respondemos 400 con un listado claro - if (!errors.isEmpty()) { - return res.status(400).json({ - message: "Error de validación", - // 5) Solo mostramos el primer error por campo (más limpio para el front) - errors: errors.array({ onlyFirstError: true }).map((e: any) => ({ - field: e.param, // ← NOMBRE DEL CAMPO (p.ej., "title", "id") - message: e.msg, // ← MENSAJE que pusiste con .withMessage(...) - location: e.location, // ← DONDE falló: "body" | "params" | "query" - })), - }); - } +// // 4) Si hay errores, respondemos 400 con un listado claro +// if (!errors.isEmpty()) { +// return res.status(400).json({ +// message: "Error de validación", +// // 5) Solo mostramos el primer error por campo (más limpio para el front) +// errors: errors.array({ onlyFirstError: true }).map((e: any) => ({ +// field: e.param, // ← NOMBRE DEL CAMPO (p.ej., "title", "id") +// message: e.msg, // ← MENSAJE que pusiste con .withMessage(...) +// location: e.location, // ← DONDE falló: "body" | "params" | "query" +// })), +// }); +// } - // 6) Si no hay errores, seguimos al siguiente middleware/controlador - next(); -} +// // 6) Si no hay errores, seguimos al siguiente middleware/controlador +// next(); +// } diff --git a/src/middlewares/authMiddlewares.ts b/src/middlewares/authMiddlewares.ts index ead0df3..5114b39 100644 --- a/src/middlewares/authMiddlewares.ts +++ b/src/middlewares/authMiddlewares.ts @@ -93,7 +93,7 @@ export function handleValidation(req: Request, res: Response, next: NextFunction }); return res.status(422).json({ - message: "Errores de validación", + message: "Faltan datos necesarios", errors: flatErrors, }); } diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 0781f3d..6e2e4c0 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -2,7 +2,7 @@ import express from 'express'; import {getAllArticles,getArticleById, deleteArticle,createArticle,updateArticle} from '../controllers/ArticleController.js'; import { authMiddleware, requireRole, handleValidation} from '../middlewares/authMiddlewares.js'; import { createArticleValidators, updateArticleValidators, idParamValidators } from '../validators/articleValidators.js'; -import { checkValidations } from "../middlewares/articleMiddlewares.js"; +// import { checkValidations } from "../middlewares/articleMiddlewares.js"; @@ -10,11 +10,11 @@ const articleRouter = express.Router(); // Rutas públicas (sin autenticación) articleRouter.get("/", getAllArticles); -articleRouter.get("/:id", idParamValidators, checkValidations, getArticleById,); +articleRouter.get("/:id", idParamValidators, getArticleById,); // Rutas protegidas (handleValidation, createArticle, createArticleValidators, checkValidations); articleRouter.post("/", authMiddleware, requireRole("admin"), createArticleValidators, handleValidation, createArticle,); -articleRouter.put("/:id", authMiddleware, requireRole("admin"), idParamValidators, updateArticleValidators, handleValidation, checkValidations, updateArticle,); +articleRouter.put("/:id", authMiddleware, requireRole("admin"), idParamValidators, updateArticleValidators, handleValidation, updateArticle,); articleRouter.delete("/:id",authMiddleware, requireRole("admin"), idParamValidators, handleValidation, deleteArticle); diff --git a/test/article.test.ts b/test/article.test.ts index 8f7d497..518646f 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -1,36 +1,19 @@ -import supertest from "supertest"; -import { app } from "../src/app.js"; -import { Article } from "../src/models/ArticleModel.js" +import supertest from 'supertest'; +import { app } from '../src/app'; // Asegúrate de que la ruta sea correcta a tu archivo principal de la aplicación +import { User } from '../src/models/UserModel'; // Asegúrate de que tu modelo de usuario esté configurado correctamente +import { Article } from '../src/models/ArticleModel'; +import jwt from 'jsonwebtoken'; + -//Creamos el "agente" de supertest para enviar requests a la app. const request = supertest(app); -//Aquí guardaremos el token del admin que creemos en beforeAll. +// Simulamos un usuario admin para las pruebas let adminToken: string; +let adminUserId: string; -// 5) Un helper pequeñito para crear payloads de artículo sin repetir mucho. -const makeArticle = (overrides: Partial> = {}) => ({ - title: "Nuevo artículo", - description: "Descripción válida", - content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", - category: "General", - species: "Animal", - image: "https://imagen.com", - references: "https://referencia.com", - creator_id: "admin_id", - ...overrides, -}); - - -describe("Rutas de Artículos", () => { - // Antes de todos los tests: - // Registramos un usuario admin - // Guardamos su token para las rutas protegidas (POST/PUT/DELETE). - - let adminId: string; // Para almacenar el ID del usuario administrador - - beforeAll(async () => { - const adminUser = { +beforeAll(async () => { + // 1. Crea un usuario admin para obtener su token de autenticación + const adminUser = { username: "adminuser", name: "Admin", last_name: "User", @@ -39,88 +22,127 @@ describe("Rutas de Artículos", () => { role: "admin", }; - // Registramos al admin + // Registramos al admin (esto depende de tu implementación de registro) const res = await request.post("/auth/register").send(adminUser); - // Verificamos la respuesta y el token - console.log("Admin User Registered:", res.body); - + // Aseguramos que la respuesta contiene un token + expect(res.body.token).toBeDefined(); + adminToken = res.body.token; + // adminUserId = res.body.user.id; // Guarda el userId para futuras validaciones + const decoded: any = jwt.verify(adminToken, process.env.JWT_SECRET!); + adminUserId = decoded.userId; // Ahora tienes el userId correctamente - if (res.body.token) { - adminToken = res.body.token; - adminId = res.body.user.id; // Guardamos el ID del administrador - console.log("Admin Token:", adminToken); - } else { - console.log("Error: No token found in response."); - } }); - // Variable para guardar el id del artículo que creemos. - let createdId: string; - - // TEST: crear artículo (POST /articles). - it("POST /articles — crea un artículo (201) y lo devuelve", async () => { - // Preparamos el body del artículo. - const body = makeArticle({ - creator_id: adminId, // Asigna el ID del admin - }); - console.log("Cuerpo del artículo:", body); // Verifica los datos enviados - console.log("Longitud de `content`:", body.content.length); // Verifica la longitud de content +// Test para la creación de artículo (POST /articles) +describe('POST /article', () => { - // Enviamos la petición con el token de admin. + it('should create an article and assign creator_id automatically', async () => { + // 2. Preparamos el artículo sin pasar creator_id + const articleData = { + title: "Nuevo Artículo de Prueba", + description: "Descripción válida", + content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", + category: "General", + species: "Animal", + image: "https://imagen.com", + references: "https://referencia.com", + }; + + // 3. Hacemos la solicitud para crear un artículo const res = await request .post("/article") - .set("Authorization", `Bearer ${adminToken}`) - .send(body); + .set("Authorization", `Bearer ${adminToken}`) // Usamos el token de autenticación + .send(articleData); - console.log("Response body:", res.body); // Verifica la respuesta del servidor - // Comprobamos status y propiedades básicas. - expect(res.status).toBe(201); - expect(res.body).toHaveProperty("id"); // Sequelize debería volver con ID + // 4. Verificamos la respuesta + expect(res.status).toBe(201); // 201 significa que el artículo se creó correctamente + expect(res.body).toHaveProperty("id"); // El artículo debe tener un ID expect(res.body).toMatchObject({ - title: body.title, - description: body.description, - content: body.content, - category: body.category, - species: body.species, - image: body.image, - references: body.references, - creator_id: body.creator_id, + title: articleData.title, + description: articleData.description, + content: articleData.content, + category: articleData.category, + species: articleData.species, + image: articleData.image, + references: articleData.references, + creator_id: String(adminUserId), // Verificamos que el creator_id sea el ID del usuario admin }); - - // Guardamos el id para los siguientes tests. - createdId = String(res.body.id); - }); - // TEST: POST /article - error 500 -it("POST /article — debe devolver un error 500 si falla la creación", async () => { - // Preparamos un body válido (igual que tu test exitoso) - const body = makeArticle({ - creator_id: adminId, - title: "FORCE_ERROR", // solo para identificar este caso si quieres }); - // Forzamos que Article.create lance un error temporalmente - const originalCreate = Article.create; - Article.create = async () => { - throw new Error("Error forzado para test 500"); - }; + it('should return 422 if required fields are missing', async () => { + // 5. Probamos si falta un campo esencial (por ejemplo, description) + const articleData = { + title: "Nuevo Artículo sin Descripción", + content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", + category: "General", + species: "Animal", + image: "https://imagen.com", + references: "https://referencia.com", + }; - const res = await request - .post("/article") - .set("Authorization", `Bearer ${adminToken}`) - .send(body); + const res = await request + .post("/article") + .set("Authorization", `Bearer ${adminToken}`) + .send(articleData); - expect(res.status).toBe(500); - expect(res.body).toHaveProperty("message", "No se pudo crear el artículo"); + // 6. Verificamos que se devuelva un error 400 si falta un campo + expect(res.status).toBe(422); + expect(res.body.message).toBe("Faltan datos necesarios"); + }); - // Restauramos la función original para que no afecte otros tests - Article.create = originalCreate; -}); + it('should return 401 if the user is not authenticated', async () => { + // 7. Probamos un caso donde el usuario no está autenticado + const articleData = { + title: "Artículos de prueba", + description: "Descripción válida", + content: "Contenido válido para la prueba de creación.", + category: "General", + species: "Animal", + image: "https://imagen.com", + references: "https://referencia.com", + }; + const res = await request + .post("/article") + .send(articleData); // No se pasa el token + // 8. Verificamos que se devuelva un error 401 si no está autenticado + expect(res.status).toBe(401); + expect(res.body.message).toBe( "No se proporcionó token de autenticación"); + }); - + // it('should return 500 if there is an error creating the article', async () => { + // // 9. Simulamos un error en la base de datos + // const articleData = { + // title: "Forzado para error", + // description: "Este artículo causará un error.", + // content: "Contenido para forzar error.", + // category: "General", + // species: "Animal", + // image: "https://imagen.com", + // references: "https://referencia.com", + // }; + + // // Simulamos un error en el modelo (por ejemplo, fallo en la creación) + // const originalCreate = Article.create; + // Article.create = async () => { + // throw new Error("Error forzado en la base de datos"); + // }; + + // const res = await request + // .post("/article") + // .set("Authorization", `Bearer ${adminToken}`) + // .send(articleData); + + // // Verificamos que se devuelva un error 500 + // expect(res.status).toBe(500); + // expect(res.body.message).toBe("No se pudo crear el artículo, Error desconocido"); + + // // Restauramos la función original para no afectar otros tests + // Article.create = originalCreate; + // }); - }); +}); diff --git a/test/auth.test.ts b/test/auth.test.ts index 7e1c413..98020be 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -24,13 +24,13 @@ describe("POST /auth/register", () => { // 2. Comprobamos que la respuesta tenga un token expect(response.body).toHaveProperty("token"); - // 3. Comprobamos que el usuario devuelto sea el correcto (sin la contraseña) - expect(response.body.user).toMatchObject({ - username: newUser.username, - email: newUser.email, - name: newUser.name, - last_name: newUser.last_name - }); + // // 3. Comprobamos que el usuario devuelto sea el correcto (sin la contraseña) + // expect(response.body.user).toMatchObject({ + // username: newUser.username, + // email: newUser.email, + // name: newUser.name, + // last_name: newUser.last_name + // }); }); it("debería devolver un error 409 si el email ya existe", async () => { @@ -101,7 +101,7 @@ describe("POST /auth/login", () => { expect(response.status).toBe(200); expect(response.body).toHaveProperty("token"); - expect(response.body.user.email).toBe(credentials.email); + // expect(response.body.user.email).toBe(credentials.email); }); it("debería devolver un error 401 con contraseña incorrecta", async () => { From 2909e0371e355babb04e7e002b4f0a549ea5f71e Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 7 Oct 2025 09:58:09 +0200 Subject: [PATCH 33/55] test(api): add CRUD method tests with authentication --- src/controllers/ArticleController.ts | 30 ++- test/article.test.ts | 296 ++++++++++++++++++--------- 2 files changed, 216 insertions(+), 110 deletions(-) diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index c989752..67170e2 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -2,16 +2,29 @@ import { Article } from "../models/ArticleModel.js"; import type { Request, Response} from "express"; -export const getAllArticles = async (_req: Request, res: Response) => { - try { - const articles = await Article.findAll() - res.status(200).json(articles) - } catch (error) { - res.status(500).json({ message: "Error obteniendo artículos", error }); + +// export const getAllArticles = async (_req: Request, res: Response) => { +// try { +// const articles = await Article.findAll() +// res.status(200).json(articles) +// } catch (error) { + +// res.status(500).json({ message: "Error obteniendo artículos", error }); +// } +// }; +export const getAllArticles = async (_req: Request, res: Response) => { + try { + const articles = await Article.findAll(); + if (!articles || articles.length === 0) { + return res.status(404).json({ message: 'No se encontraron artículos' }); // Manejo explícito de error } + res.status(200).json(articles); // Devuelve los artículos + } catch (error) { + console.error('Error obteniendo artículos:', error); + res.status(500).json({ message: 'Error obteniendo artículos', error }); + } }; - export const getArticleById = async (req: Request<{ id: string }>, res: Response) => { try { @@ -55,7 +68,8 @@ export const createArticle = async (req: Request, res: Response) => { // Aquí filtramos los campos que sí queremos guardar const {title, description, content, category, species, image, references, } = req.body; // Verifica si algún campo esencial falta - const creator_id = BigInt(req.user.userId).toString(); // Convierte BigInt a string si es necesario + // const creator_id = BigInt(req.user.userId).toString(); + const creator_id = BigInt(req.user.userId)// Convierte BigInt a string si es necesario console.log("Creador ID:", creator_id); // Verifica que el creator_id sea correcto diff --git a/test/article.test.ts b/test/article.test.ts index 518646f..f4dc9c1 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -1,64 +1,82 @@ import supertest from 'supertest'; import { app } from '../src/app'; // Asegúrate de que la ruta sea correcta a tu archivo principal de la aplicación -import { User } from '../src/models/UserModel'; // Asegúrate de que tu modelo de usuario esté configurado correctamente -import { Article } from '../src/models/ArticleModel'; import jwt from 'jsonwebtoken'; const request = supertest(app); +beforeAll(() => { + console.log('El servidor de pruebas se está levantando...'); +}); -// Simulamos un usuario admin para las pruebas -let adminToken: string; -let adminUserId: string; +// Variables globales para los tests +let adminToken: string; // Token del administrador +let adminUserId: string; // ID del administrador (decodificado del token) +let seededArticleId: string; // ID del artículo creado para pruebas +// Función para generar datos de prueba de artículos +function makeArticleData(overrides: Partial> = {}) { + const longContent = 'Este es un contenido de prueba suficientemente largo para pasar validaciones de longitud. '.repeat(3).trim(); // Usamos `.trim()` para evitar espacios adicionales + + return { + title: `Artículo de prueba ${Date.now()}`, + description: 'Una descripción válida para el artículo de prueba', + content: longContent, // Ahora aseguramos que no haya espacios extra + category: 'General', + species: 'Animal', + image: 'https://ejemplo.com/imagen.jpg', + references: 'https://ejemplo.com/ref', + ...overrides, + }; +} +// Antes de todos los tests beforeAll(async () => { - // 1. Crea un usuario admin para obtener su token de autenticación + // Crear un usuario admin const adminUser = { - username: "adminuser", - name: "Admin", - last_name: "User", - email: "admin.articles@example.com", - password: "password123", - role: "admin", + username: `admin_${Date.now()}`, + name: 'Admin', + last_name: 'User', + email: `admin_${Date.now()}@example.com`, + password: 'password123', + role: 'admin', }; - // Registramos al admin (esto depende de tu implementación de registro) - const res = await request.post("/auth/register").send(adminUser); - - // Aseguramos que la respuesta contiene un token - expect(res.body.token).toBeDefined(); - adminToken = res.body.token; - // adminUserId = res.body.user.id; // Guarda el userId para futuras validaciones - const decoded: any = jwt.verify(adminToken, process.env.JWT_SECRET!); - adminUserId = decoded.userId; // Ahora tienes el userId correctamente - + // Registramos al admin + const resRegister = await request.post('/auth/register').send(adminUser); + expect(resRegister.status).toBe(201); + expect(resRegister.body.token).toBeDefined(); + adminToken = resRegister.body.token; + + // Decodificamos el token para obtener el userId + const decoded = jwt.decode(adminToken) as any; + adminUserId = decoded?.userId?.toString() ?? decoded?.userId; + expect(adminUserId).toBeTruthy(); + + // Sembramos un artículo para usarlo en GET/PUT/DELETE + const seedData = makeArticleData({ title: 'Artículo sembrado para pruebas' }); + const resSeed = await request + .post('/article') + .set('Authorization', `Bearer ${adminToken}`) + .send(seedData); + expect(resSeed.status).toBe(201); + expect(resSeed.body.id).toBeDefined(); + seededArticleId = resSeed.body.id.toString(); }); +// ----------------- +// POST /article +// ----------------- -// Test para la creación de artículo (POST /articles) describe('POST /article', () => { + it('crea un artículo y asigna creator_id automáticamente', async () => { + const articleData = makeArticleData({ title: 'Nuevo Artículo de Prueba (POST OK)' }); - it('should create an article and assign creator_id automatically', async () => { - // 2. Preparamos el artículo sin pasar creator_id - const articleData = { - title: "Nuevo Artículo de Prueba", - description: "Descripción válida", - content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", - category: "General", - species: "Animal", - image: "https://imagen.com", - references: "https://referencia.com", - }; - - // 3. Hacemos la solicitud para crear un artículo const res = await request - .post("/article") - .set("Authorization", `Bearer ${adminToken}`) // Usamos el token de autenticación + .post('/article') + .set('Authorization', `Bearer ${adminToken}`) .send(articleData); - // 4. Verificamos la respuesta - expect(res.status).toBe(201); // 201 significa que el artículo se creó correctamente - expect(res.body).toHaveProperty("id"); // El artículo debe tener un ID + expect(res.status).toBe(201); + expect(res.body).toHaveProperty('id'); expect(res.body).toMatchObject({ title: articleData.title, description: articleData.description, @@ -67,82 +85,156 @@ describe('POST /article', () => { species: articleData.species, image: articleData.image, references: articleData.references, - creator_id: String(adminUserId), // Verificamos que el creator_id sea el ID del usuario admin + creator_id: String(adminUserId), }); }); - it('should return 422 if required fields are missing', async () => { - // 5. Probamos si falta un campo esencial (por ejemplo, description) - const articleData = { - title: "Nuevo Artículo sin Descripción", - content: "Este es un contenido de prueba con exactamente cien caracteres para la validación, si no los tiene nunca pasará los test porque en las validaciones le hemos puesto ese requisito.", - category: "General", - species: "Animal", - image: "https://imagen.com", - references: "https://referencia.com", - }; + it('devuelve 422 si faltan campos requeridos (description, por ejemplo)', async () => { + const articleData = makeArticleData({ description: '' }); const res = await request - .post("/article") - .set("Authorization", `Bearer ${adminToken}`) + .post('/article') + .set('Authorization', `Bearer ${adminToken}`) .send(articleData); - // 6. Verificamos que se devuelva un error 400 si falta un campo expect(res.status).toBe(422); - expect(res.body.message).toBe("Faltan datos necesarios"); + expect(res.body.message).toBe('Faltan datos necesarios'); }); - it('should return 401 if the user is not authenticated', async () => { - // 7. Probamos un caso donde el usuario no está autenticado - const articleData = { - title: "Artículos de prueba", - description: "Descripción válida", - content: "Contenido válido para la prueba de creación.", - category: "General", - species: "Animal", - image: "https://imagen.com", - references: "https://referencia.com", - }; + it('devuelve 401 si NO estás autenticado (si el middleware protege la ruta)', async () => { + const articleData = makeArticleData({ title: 'Debe fallar por no auth' }); - const res = await request - .post("/article") - .send(articleData); // No se pasa el token + const res = await request.post('/article').send(articleData); - // 8. Verificamos que se devuelva un error 401 si no está autenticado expect(res.status).toBe(401); - expect(res.body.message).toBe( "No se proporcionó token de autenticación"); + expect(res.body.message).toMatch(/token/i); }); +}); + +// ----------------- +// GET /articles +// ----------------- + +describe('GET /article', () => { + it('devuelve 200 y un array de artículos', async () => { + const res = await request.get('/article'); + console.log(res.body); // Imprime la respuesta de la API para ver qué se está recibiendo + + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); // tenemos al menos el sembrado + }); +}); + +// -------------------------- +// GET /article/:id (uno) +// -------------------------- + + +describe('GET /article/:id', () => { +it('devuelve 200 y el artículo si existe', async () => { +const res = await request.get(`/article/${seededArticleId}`); +expect(res.status).toBe(200); +expect(res.body).toHaveProperty('id'); +expect(String(res.body.id)).toBe(String(seededArticleId)); +}); - // it('should return 500 if there is an error creating the article', async () => { - // // 9. Simulamos un error en la base de datos - // const articleData = { - // title: "Forzado para error", - // description: "Este artículo causará un error.", - // content: "Contenido para forzar error.", - // category: "General", - // species: "Animal", - // image: "https://imagen.com", - // references: "https://referencia.com", - // }; - - // // Simulamos un error en el modelo (por ejemplo, fallo en la creación) - // const originalCreate = Article.create; - // Article.create = async () => { - // throw new Error("Error forzado en la base de datos"); - // }; - - // const res = await request - // .post("/article") - // .set("Authorization", `Bearer ${adminToken}`) - // .send(articleData); - - // // Verificamos que se devuelva un error 500 - // expect(res.status).toBe(500); - // expect(res.body.message).toBe("No se pudo crear el artículo, Error desconocido"); - - // // Restauramos la función original para no afectar otros tests - // Article.create = originalCreate; - // }); +it('devuelve 404 si el artículo no existe', async () => { +const res = await request.get('/article/99999999'); // id grande que no exista +expect(res.status).toBe(404); +expect(res.body.message).toBe('Artículo no encontrado'); +}); }); +// -------------------------- +// PUT /article/:id (update) +// -------------------------- + + +describe('PUT /article/:id', () => { +it('actualiza campos permitidos y devuelve 200 con el artículo actualizado', async () => { +const newTitle = 'Título actualizado vía PUT'; + + +const res = await request +.put(`/article/${seededArticleId}`) +.set('Authorization', `Bearer ${adminToken}`) +.send({ title: newTitle, category: 'Actualizada' }); + + +// Tu controlador devuelve { message, article } +expect(res.status).toBe(200); +expect(res.body.message).toBe('Artículo actualizado correctamente'); +expect(res.body.article).toBeDefined(); +expect(res.body.article.title).toBe(newTitle); +expect(res.body.article.category).toBe('Actualizada'); +}); + + +it('devuelve 404 si intentas actualizar un artículo que no existe', async () => { +const res = await request +.put('/article/99999999') +.set('Authorization', `Bearer ${adminToken}`) +.send({ title: 'No debería existir' }); + + +expect(res.status).toBe(404); +expect(res.body.message).toBe('Artículo no encontrado'); +}); + + +it('devuelve 401 si no envías token (si la ruta está protegida por middleware)', async () => { +const res = await request +.put(`/article/${seededArticleId}`) +.send({ title: 'Debe fallar por no auth' }); + +expect([200, 401]).toContain(res.status); +}); +}); + +// ----------------------------- +// DELETE /article/:id (delete) +// ----------------------------- + + +describe('DELETE /article/:id', () => { +it('borra un artículo existente y devuelve 200', async () => { +// Creamos un artículo NUEVO solo para borrarlo aquí +const temp = await request +.post('/article') +.set('Authorization', `Bearer ${adminToken}`) +.send(makeArticleData({ title: 'Para borrar en DELETE' })); + + +expect(temp.status).toBe(201); +const idToDelete = temp.body.id; + + +const res = await request +.delete(`/article/${idToDelete}`) +.set('Authorization', `Bearer ${adminToken}`); + + +expect(res.status).toBe(200); +expect(res.body.message).toBe('El articulo esta eliminado correctamente'); +}); + + +it('devuelve 404 si intentas borrar un artículo que no existe', async () => { +const res = await request +.delete('/article/99999999') +.set('Authorization', `Bearer ${adminToken}`); + + +expect(res.status).toBe(404); +expect(res.body.message).toBe('Artículo no encontrado'); +}); + + +it('devuelve 401 si no envías token (si hay middleware de auth)', async () => { +const res = await request.delete(`/article/${seededArticleId}`); + +expect([200, 401, 404]).toContain(res.status); +}); +}); \ No newline at end of file From 511d31cd6f2010855159b760e11d25bf43d8fc7e Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 7 Oct 2025 10:17:25 +0200 Subject: [PATCH 34/55] fix(controller): adjust logic in ArticleController --- src/controllers/ArticleController.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index 67170e2..a36e008 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -2,17 +2,6 @@ import { Article } from "../models/ArticleModel.js"; import type { Request, Response} from "express"; - - -// export const getAllArticles = async (_req: Request, res: Response) => { -// try { -// const articles = await Article.findAll() -// res.status(200).json(articles) -// } catch (error) { - -// res.status(500).json({ message: "Error obteniendo artículos", error }); -// } -// }; export const getAllArticles = async (_req: Request, res: Response) => { try { const articles = await Article.findAll(); @@ -68,17 +57,15 @@ export const createArticle = async (req: Request, res: Response) => { // Aquí filtramos los campos que sí queremos guardar const {title, description, content, category, species, image, references, } = req.body; // Verifica si algún campo esencial falta - // const creator_id = BigInt(req.user.userId).toString(); - const creator_id = BigInt(req.user.userId)// Convierte BigInt a string si es necesario + const creator_id = BigInt(req.user.userId).toString();// Convierte BigInt a string si es necesario console.log("Creador ID:", creator_id); // Verifica que el creator_id sea correcto if (!title || !description || !content || !category || !species ) { console.error("Faltan datos necesarios para crear el artículo"); return res.status(400).json({ message: "Faltan datos necesarios" }); - } - - + } + // Creamos el artículo solo con esos campos (los demás se ignoran) const newArticle = await Article.create({ From cf8bc8317a97a2a9e38471a2e217b76d89726cff Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 7 Oct 2025 11:54:25 +0200 Subject: [PATCH 35/55] feat: refactor article model - split into tables, tests, and validators --- src/app.ts | 2 +- src/controllers/ArticleController.ts | 2 +- src/interface/articleInterface.ts | 14 +++++ src/interface/userInterface.ts | 11 ++++ src/models/ArticleModel.ts | 84 +++++++++++----------------- src/models/UserModel.ts | 17 ++---- src/validators/articleValidators.ts | 14 +++-- test/article.test.ts | 28 +++++----- 8 files changed, 87 insertions(+), 85 deletions(-) create mode 100644 src/interface/articleInterface.ts create mode 100644 src/interface/userInterface.ts diff --git a/src/app.ts b/src/app.ts index b270237..78b0fb5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -25,7 +25,7 @@ async function startServer() { try { // Sincroniza los modelos con la base de datos await db_connection.sync(); // OJO: Ver las opciones más abajo - console.log("✅ Database synchronized successfully."); + console.log("✅ Database synchronized successfully."); app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index a36e008..57f20ba 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -57,7 +57,7 @@ export const createArticle = async (req: Request, res: Response) => { // Aquí filtramos los campos que sí queremos guardar const {title, description, content, category, species, image, references, } = req.body; // Verifica si algún campo esencial falta - const creator_id = BigInt(req.user.userId).toString();// Convierte BigInt a string si es necesario + const creator_id = Number(req.user.userId); console.log("Creador ID:", creator_id); // Verifica que el creator_id sea correcto diff --git a/src/interface/articleInterface.ts b/src/interface/articleInterface.ts new file mode 100644 index 0000000..721efad --- /dev/null +++ b/src/interface/articleInterface.ts @@ -0,0 +1,14 @@ +export interface ArticleAttributes { + id: number; + creator_id: number; //TENER EN CUENTA QUE DEBE VENIR DEL MODELO DE USER + title: string; + description: string; + content:string; + category: string; + species: string; + image?: string; + references?: string; +// likes_count: bigint; + created_at: Date; + updated_at: Date; +} \ No newline at end of file diff --git a/src/interface/userInterface.ts b/src/interface/userInterface.ts new file mode 100644 index 0000000..4502bba --- /dev/null +++ b/src/interface/userInterface.ts @@ -0,0 +1,11 @@ +export interface UserAttributes { + id: number; + username: string; + email: string; + password: string; + name: string; + last_name: string; + role: string; + created_at: Date; + updated_at: Date; +} \ No newline at end of file diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts index a4103cf..439a988 100644 --- a/src/models/ArticleModel.ts +++ b/src/models/ArticleModel.ts @@ -2,22 +2,7 @@ import { DataTypes, Model, Optional } from "sequelize"; // 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) import db_connection from "../database/db_connection.js"; - -// 3) Declaro cómo es un usuario en la BD (TODOS los campos) -export interface ArticleAttributes { - id: bigint; - creator_id: bigint; //TENER EN CUENTA QUE DEBE VENIR DEL MODELO DE USER - title: string; - description: string; - content:string; - category: string; - species: string; - image?: string; - references?: string; -// likes_count: bigint; - created_at: Date; - updated_at: Date; -} +import { ArticleAttributes } from "../interface/articleInterface.js"; // 4) Campos opcionales AL CREAR (Sequelize los rellena solo) export type ArticleCreationAttributes = Optional< @@ -30,8 +15,8 @@ export class Article extends Model implements ArticleAttributes { - declare id: bigint; - declare creator_id: bigint; + declare id: number; // Cambié de 'bigint' a 'number' (equivalente a 'integer' en JS) + declare creator_id: number; // Cambié de 'bigint' a 'number' (equivalente a 'integer' en JS) declare title: string; declare description: string; declare content: string; @@ -41,15 +26,13 @@ export class Article declare references: string; declare created_at: Date; declare updated_at: Date; - } - // 6) Inicializo (equivalente a define) y mapeo columnas/validaciones Article.init( { id: { - type: DataTypes.BIGINT, + type: DataTypes.INTEGER, // Cambié de DataTypes.BIGINT a DataTypes.INTEGER autoIncrement: true, primaryKey: true, }, @@ -58,7 +41,7 @@ Article.init( allowNull: false, validate: { notNull: { msg: "titulo no puede estar vacío" }, - len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, + len: { args: [3, 255], msg: "titulo mínimo 3 caracteres" }, }, }, description: { @@ -66,10 +49,9 @@ Article.init( allowNull: false, validate: { notNull: { msg: "descripcion no puede estar vacío" }, - len: { args: [3, 255], msg: "username mínimo 3 caracteres" }, + len: { args: [3, 255], msg: "descripcion mínimo 3 caracteres" }, }, }, - content: { type: DataTypes.TEXT("long"), allowNull: false, @@ -78,44 +60,45 @@ Article.init( len: { args: [6, 65535], msg: "content mínimo 6 caracteres" }, }, }, - category: { - type: DataTypes.STRING, - allowNull: false, - validate: { - notNull: { msg: "content no puede estar vacío" }, - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, - }, - }, - + category: { + type: DataTypes.ENUM('Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación'), + allowNull: false, + validate: { + notNull: { msg: "category no puede estar vacío" }, + isIn: { + args: [['Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación']], // Esta es la validación isIn + msg: "category debe ser uno de los siguientes: 'Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación'" + } + }, +}, species: { type: DataTypes.STRING, allowNull: false, validate: { - notNull: { msg: "content no puede estar vacío" }, - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + notNull: { msg: "species no puede estar vacío" }, + len: { args: [6, 255], msg: "species mínimo 6 caracteres" }, }, }, - image: { + image: { type: DataTypes.STRING, allowNull: true, - validate: { - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + validate: { + len: { args: [6, 255], msg: "image mínimo 6 caracteres" }, }, }, - - references: { + references: { type: DataTypes.STRING, allowNull: true, - validate: { - len: { args: [6, 255], msg: "content mínimo 6 caracteres" }, + validate: { + len: { args: [6, 255], msg: "references mínimo 6 caracteres" }, }, }, creator_id: { - type: DataTypes.BIGINT, - allowNull: false, - references: { + type: DataTypes.INTEGER, // Cambié de DataTypes.BIGINT a DataTypes.INTEGER + allowNull: false, + references: { model: 'users', - key: 'id' + key: 'id', }, }, created_at: { @@ -130,10 +113,10 @@ Article.init( }, }, { - sequelize: db_connection, // ← tu conexión - tableName: "articles", // ← nombre real de la tabla - timestamps: true, // ← activa created/updated - underscored: true, // ← columnas snake_case + sequelize: db_connection, // ← tu conexión + tableName: "articles", // ← nombre real de la tabla + timestamps: true, // ← activa created/updated + underscored: true, // ← columnas snake_case createdAt: "created_at", // ← mapea el nombre updatedAt: "updated_at", // ← mapea el nombre } @@ -141,4 +124,3 @@ Article.init( // 7) ¡Exporto el modelo! (puedes default o nombrado) export default Article; - diff --git a/src/models/UserModel.ts b/src/models/UserModel.ts index bc6f33c..7198df5 100644 --- a/src/models/UserModel.ts +++ b/src/models/UserModel.ts @@ -2,19 +2,10 @@ import { DataTypes, Model, Optional } from "sequelize"; // 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) import db_connection from "../database/db_connection.js" +import { UserAttributes } from "../interface/userInterface.js"; // 3) Declaro cómo es un usuario en la BD (TODOS los campos) -export interface UserAttributes { - id: bigint; - username: string; - email: string; - password: string; - name: string; - last_name: string; - role: string; - created_at: Date; - updated_at: Date; -} + // 4) Campos opcionales AL CREAR (Sequelize los rellena solo) export type UserCreationAttributes = Optional< @@ -27,7 +18,7 @@ export class User extends Model implements UserAttributes { - declare id: bigint; + declare id: number; declare username: string; declare email: string; declare password: string; @@ -42,7 +33,7 @@ export class User User.init( { id: { - type: DataTypes.BIGINT, + type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, diff --git a/src/validators/articleValidators.ts b/src/validators/articleValidators.ts index 1c11cbd..4ef3814 100644 --- a/src/validators/articleValidators.ts +++ b/src/validators/articleValidators.ts @@ -13,13 +13,13 @@ export const createArticleValidators = [ .trim() .isLength({ min: 3 }).withMessage("description mínimo 3 caracteres"), - // content: string mínimo 100 - body("content") - .isString().withMessage("content debe ser un string") + // category: Validación con enum + body("category") + .isString().withMessage("category debe ser un string") .trim() - .isLength({ min: 100 }).withMessage("content mínimo 100 caracteres"), + .isIn(['Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación']) + .withMessage("category debe ser uno de los siguientes: 'Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación'"), - // category: string mínimo 6 body("category") .isString().withMessage("category debe ser un string") .trim() @@ -77,7 +77,9 @@ export const updateArticleValidators = [ .optional() .isString().withMessage("category debe ser un string") .trim() - .isLength({ min: 6 }).withMessage("category mínimo 6 caracteres"), + .isIn(['Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación']) + .withMessage("category debe ser uno de los siguientes: 'Fauna Abisal', 'Ecosistemas', 'Exploración', 'Conservación'"), + body("species") .optional() diff --git a/test/article.test.ts b/test/article.test.ts index f4dc9c1..78100af 100644 --- a/test/article.test.ts +++ b/test/article.test.ts @@ -20,7 +20,7 @@ function makeArticleData(overrides: Partial> = {}) { title: `Artículo de prueba ${Date.now()}`, description: 'Una descripción válida para el artículo de prueba', content: longContent, // Ahora aseguramos que no haya espacios extra - category: 'General', + category: "Fauna Abisal", species: 'Animal', image: 'https://ejemplo.com/imagen.jpg', references: 'https://ejemplo.com/ref', @@ -77,16 +77,18 @@ describe('POST /article', () => { expect(res.status).toBe(201); expect(res.body).toHaveProperty('id'); - expect(res.body).toMatchObject({ - title: articleData.title, - description: articleData.description, - content: articleData.content, - category: articleData.category, - species: articleData.species, - image: articleData.image, - references: articleData.references, - creator_id: String(adminUserId), - }); + // Cambia en tu test, donde haces el `toMatchObject` +expect(res.body).toMatchObject({ + title: articleData.title, + description: articleData.description, + content: articleData.content, + category: articleData.category, + species: articleData.species, + image: articleData.image, + references: articleData.references, + creator_id: expect.any(Number), // Cambié de string a Number +}); + }); it('devuelve 422 si faltan campos requeridos (description, por ejemplo)', async () => { @@ -160,7 +162,7 @@ const newTitle = 'Título actualizado vía PUT'; const res = await request .put(`/article/${seededArticleId}`) .set('Authorization', `Bearer ${adminToken}`) -.send({ title: newTitle, category: 'Actualizada' }); +.send({ title: newTitle, category: 'Fauna Abisal' }); // Tu controlador devuelve { message, article } @@ -168,7 +170,7 @@ expect(res.status).toBe(200); expect(res.body.message).toBe('Artículo actualizado correctamente'); expect(res.body.article).toBeDefined(); expect(res.body.article.title).toBe(newTitle); -expect(res.body.article.category).toBe('Actualizada'); +expect(res.body.article.category).toBe("Fauna Abisal"); }); From 5f2a09506b18146fc25d8bbc8a30195837d67521 Mon Sep 17 00:00:00 2001 From: gemayc Date: Thu, 9 Oct 2025 10:26:11 +0200 Subject: [PATCH 36/55] chore(routes): testing article creation routes --- package-lock.json | 34 ++++++++++++++++++++++++++++++++++ package.json | 2 ++ src/app.ts | 3 ++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 98c770a..4cc6b27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "bcryptjs": "^3.0.2", + "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^17.2.2", "express": "^5.1.0", @@ -20,6 +21,7 @@ }, "devDependencies": { "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", @@ -1627,6 +1629,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2784,6 +2796,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -5148,6 +5173,15 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index adaadf0..d14a3fd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "description": "", "dependencies": { "bcryptjs": "^3.0.2", + "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^17.2.2", "express": "^5.1.0", @@ -33,6 +34,7 @@ }, "devDependencies": { "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", diff --git a/src/app.ts b/src/app.ts index d96ae6b..0c78127 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,6 +9,7 @@ import { User } from "./models/UserModel.js"; import { Article } from "./models/ArticleModel.js"; import passwordResetRouter from "./routes/passwordReset.routes.js"; import "./models/PasswordResetToken.js"; +import cors from "cors"; User.hasMany(Article, { foreignKey: 'creator_id' }); @@ -16,7 +17,7 @@ Article.belongsTo(User, { foreignKey: 'creator_id' }); export const app = express(); const PORT = process.env.PORT || 8080; - + app.use(cors({ origin: "http://localhost:5174" })); // puerto de Vite app.use(express.json()); app.get("/", (_req, res) => { res.send("Hola API"); From 0db9e1421acbb9b68f4ebf5dc67593352f5b0c94 Mon Sep 17 00:00:00 2001 From: gemayc Date: Thu, 9 Oct 2025 13:58:05 +0200 Subject: [PATCH 37/55] feat(api): add route to identify authenticated user --- src/app.ts | 3 ++- src/middlewares/authMiddlewares.ts | 2 +- src/routes/articleRoutes.ts | 6 ++--- src/routes/userRoutes.ts | 37 ++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/routes/userRoutes.ts diff --git a/src/app.ts b/src/app.ts index 0c78127..8bc7471 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,7 @@ import { Article } from "./models/ArticleModel.js"; import passwordResetRouter from "./routes/passwordReset.routes.js"; import "./models/PasswordResetToken.js"; import cors from "cors"; +import userRouter from "../src/routes/userRoutes.js"; // 👈 el nuevo archivo User.hasMany(Article, { foreignKey: 'creator_id' }); @@ -24,7 +25,7 @@ Article.belongsTo(User, { foreignKey: 'creator_id' }); }); app.use("/auth", authRouter ) app.use("/article", articleRouter) - +app.use(userRouter); // /user/:id app.use("/auth", passwordResetRouter); await db_connection.sync({ alter: true }); // o { force: true } si quieres regenerar diff --git a/src/middlewares/authMiddlewares.ts b/src/middlewares/authMiddlewares.ts index 5114b39..1643868 100644 --- a/src/middlewares/authMiddlewares.ts +++ b/src/middlewares/authMiddlewares.ts @@ -57,7 +57,7 @@ export const authMiddleware = ( /** * Middleware que verifica si el usuario tiene un rol específico */ -export const requireRole = (...allowedRoles: string[]) => { +export const requireRole = (allowedRoles: Array) => { return (req: Request, res: Response, next: NextFunction): void => { if (!req.user) { res.status(401).json({ message: "Usuario no autorizado" }); diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 6e2e4c0..231aab7 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -13,9 +13,9 @@ articleRouter.get("/", getAllArticles); articleRouter.get("/:id", idParamValidators, getArticleById,); // Rutas protegidas (handleValidation, createArticle, createArticleValidators, checkValidations); -articleRouter.post("/", authMiddleware, requireRole("admin"), createArticleValidators, handleValidation, createArticle,); -articleRouter.put("/:id", authMiddleware, requireRole("admin"), idParamValidators, updateArticleValidators, handleValidation, updateArticle,); -articleRouter.delete("/:id",authMiddleware, requireRole("admin"), idParamValidators, handleValidation, deleteArticle); +articleRouter.post("/", authMiddleware, requireRole(["admin", "user"]), createArticleValidators, handleValidation, createArticle,); +articleRouter.put("/:id", authMiddleware, requireRole(["admin"]), idParamValidators, updateArticleValidators, handleValidation, updateArticle,); +articleRouter.delete("/:id",authMiddleware, requireRole(["admin"]), idParamValidators, handleValidation, deleteArticle); export default articleRouter; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts new file mode 100644 index 0000000..41c3913 --- /dev/null +++ b/src/routes/userRoutes.ts @@ -0,0 +1,37 @@ +import { Router, Request, Response } from "express"; +import User from "../models/UserModel.js" + +const router = Router(); + +/** + * GET /user/:id + * Devuelve datos públicos del usuario (id, username, name) + */ +router.get("/user/:id", async (req: Request, res: Response) => { + try { + const id = Number(req.params.id); + + // 1️⃣ Validamos que el ID sea un número válido + if (Number.isNaN(id) || id < 1) { + return res.status(400).json({ message: "id inválido" }); + } + + // 2️⃣ Buscamos al usuario en la base de datos + const user = await User.findByPk(id, { + attributes: ["id", "username", "name"], // 👈 solo devolvemos datos públicos + }); + + // 3️⃣ Si no existe, devolvemos 404 + if (!user) { + return res.status(404).json({ message: "Usuario no encontrado" }); + } + + // 4️⃣ Si todo va bien, devolvemos el usuario en formato JSON + return res.json(user); + } catch (error) { + console.error("Error en GET /user/:id:", error); + return res.status(500).json({ message: "Error al obtener usuario" }); + } +}); + +export default router; From ea3d111e70519d05806b402d6314802ba32922b2 Mon Sep 17 00:00:00 2001 From: gemayc Date: Fri, 10 Oct 2025 10:16:14 +0200 Subject: [PATCH 38/55] change in routes --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 8bc7471..7780f1f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,7 +17,7 @@ User.hasMany(Article, { foreignKey: 'creator_id' }); Article.belongsTo(User, { foreignKey: 'creator_id' }); export const app = express(); - const PORT = process.env.PORT || 8080; + const PORT = process.env.PORT || 8000; app.use(cors({ origin: "http://localhost:5174" })); // puerto de Vite app.use(express.json()); app.get("/", (_req, res) => { From bb2366b4266c6fe54e7a52e47c56d90c959c5b8d Mon Sep 17 00:00:00 2001 From: gemayc Date: Fri, 10 Oct 2025 18:02:58 +0200 Subject: [PATCH 39/55] build(docker): add Dockerfile and docker-compose to containerize API --- .dockerignore | 12 ++ .env.example | 14 +- .gitignore | 3 +- Dockerfile | 36 ++++++ backup.sql | 0 docker-compose.yml | 35 +++++ src/app.ts | 14 +- src/controllers/AuthController.ts | 4 +- src/controllers/PasswordResetController.ts | 144 ++++++++++++++++++--- src/utils/jwt.ts | 23 +++- tsconfig.json | 4 +- 11 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 backup.sql create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4dd6ea6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +coverage +.tmp +.cache +.git +.gitignore +Dockerfile* +docker-compose* +.env +.env.* +.env.docker diff --git a/.env.example b/.env.example index 78d0283..7462df1 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ -DB_NAME= -DB_USER= -DB_PASS= +DB_NAME= +DB_USER= +DB_PASS= DB_HOST= -DIALECT= -PORT= \ No newline at end of file +DB_DIALECT= +PORT= +CORS_ORIGIN= + +JWT_SECRET= +JWT_EXPIRES = \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c1a83c..6008da1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules .env .env.test .DS_Store -dist \ No newline at end of file +dist +.env.docker \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cdf28bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# ========= Etapa 1: dependencias de producción ========= +FROM node:20-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +# Comentario: +# - 'npm ci' instala exactamente lo que hay en package-lock. +# - '--omit=dev' deja solo deps de producción (más pequeño). + +# ========= Etapa 2: compilar TypeScript a dist/ ========= +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci # aquí sí instalamos devDependencies (ts, tipos, etc.) +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build # genera dist/app.js (coincide con tu package.json) + +# ========= Etapa 3: imagen final para ejecutar ========= +FROM node:20-alpine AS runner +ENV NODE_ENV=production +WORKDIR /app + +# Copiamos lo mínimo necesario para correr +COPY --from=deps /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY package*.json ./ + +# Seguridad: no correr como root +RUN addgroup -S app && adduser -S app -G app +RUN chown -R app:app /app +USER app + +EXPOSE 8000 +# Muy importante: en Render se usa process.env.PORT automáticamente +CMD ["node", "dist/app.js"] diff --git a/backup.sql b/backup.sql new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b0d22ac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +services: + mysql: + image: mysql:8.4 + container_name: mysql-abisal + environment: + - MYSQL_ROOT_PASSWORD=alba2005 # contraseña del root + - MYSQL_DATABASE=abisal_app # base de datos que se crea al iniciar + - MYSQL_USER=app # usuario normal (NO root) + - MYSQL_PASSWORD=alba2005 # contraseña app + ports: + - "3307:3306" # mapeo externo (puedes dejarlo o quitarlo) + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile + container_name: abisal-api + env_file: .env.docker + environment: + - DB_HOST=mysql + ports: + - "8000:8000" + depends_on: + mysql: + condition: service_healthy + +volumes: + mysql_data: diff --git a/src/app.ts b/src/app.ts index 7780f1f..5165165 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,8 @@ import express from "express"; -import db_connection from "../src/database/db_connection.js"; +import db_connection from "./database/db_connection.js"; import "dotenv/config"; -import "./models/UserModel"; -import "./models/ArticleModel"; +import "./models/UserModel.js"; +import "./models/ArticleModel.js"; import authRouter from "./routes/authRoutes.js"; import articleRouter from "./routes/articleRoutes.js"; import { User } from "./models/UserModel.js"; @@ -10,7 +10,7 @@ import { Article } from "./models/ArticleModel.js"; import passwordResetRouter from "./routes/passwordReset.routes.js"; import "./models/PasswordResetToken.js"; import cors from "cors"; -import userRouter from "../src/routes/userRoutes.js"; // 👈 el nuevo archivo +import userRouter from "./routes/userRoutes.js"; // 👈 el nuevo archivo User.hasMany(Article, { foreignKey: 'creator_id' }); @@ -18,7 +18,7 @@ Article.belongsTo(User, { foreignKey: 'creator_id' }); export const app = express(); const PORT = process.env.PORT || 8000; - app.use(cors({ origin: "http://localhost:5174" })); // puerto de Vite + app.use(cors({ origin: process.env.CORS_ORIGIN || "*" })); // puerto de Vite app.use(express.json()); app.get("/", (_req, res) => { res.send("Hola API"); @@ -28,12 +28,12 @@ app.use("/article", articleRouter) app.use(userRouter); // /user/:id app.use("/auth", passwordResetRouter); -await db_connection.sync({ alter: true }); // o { force: true } si quieres regenerar +// await db_connection.sync({ alter: true }); // o { force: true } si quieres regenerar async function startServer() { try { // Sincroniza los modelos con la base de datos - await db_connection.sync(); // OJO: Ver las opciones más abajo + await db_connection.sync(); console.log("✅ Database synchronized successfully."); app.listen(PORT, () => { diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 7152d3e..d55148a 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -73,7 +73,7 @@ export const registerController = async ( // 4. Generar token JWT const token = generateToken({ - userId: newUser.id, + userId: BigInt((newUser as any).id), // forzamos bigint desde id (que suele ser number en Sequelize) email: newUser.email, role: newUser.role, }); @@ -126,7 +126,7 @@ if (!user || !(await bcrypt.compare(password, user.password))) { // Generar token JWT const token = generateToken({ - userId: user.id, + userId: BigInt((user as any).id), // forzamos bigint para que cumpla con TokenPayload.userId email: user.email, role: user.role, }); diff --git a/src/controllers/PasswordResetController.ts b/src/controllers/PasswordResetController.ts index 9639a7c..c4ce324 100644 --- a/src/controllers/PasswordResetController.ts +++ b/src/controllers/PasswordResetController.ts @@ -1,15 +1,117 @@ +// import { Request, Response } from "express"; +// import bcrypt from "bcryptjs"; +// import User from "../models/UserModel.js"; +// import PasswordResetToken from "../models/PasswordResetToken.js"; +// import { generateRawToken, hashToken } from "../utils/resetToken.js"; + +// // Enviar email (usa nodemailer en prod; aquí vamos a loguear el link en dev) +// async function sendResetEmail(to: string, url: string) { +// if (process.env.NODE_ENV === "production") { +// // TODO: integra nodemailer o tu proveedor (Sendgrid, SES, etc.) +// } else { +// console.log("🔗 Reset link (dev):", url); +// } +// } + +// export const forgotPassword = async (req: Request, res: Response): Promise => { +// const normalizedEmail = String(req.body.email).toLowerCase().trim(); + +// // Respuesta genérica SIEMPRE (no revelar si existe) +// const generic = { message: "Si el email existe, te enviaremos instrucciones para restablecer tu contraseña." }; + +// // Busca usuario +// const user = await User.findOne({ where: { email: normalizedEmail } }); +// if (!user) { +// res.status(200).json(generic); +// return; +// } + +// // Genera token +// const rawToken = generateRawToken(); +// const tokenHash = hashToken(rawToken); +// const expires = new Date(Date.now() + 15 * 60 * 1000); // 15 min + +// // Opcional: invalida tokens previos no usados +// await PasswordResetToken.update( +// { used_at: new Date() }, +// { where: { user_id: user.id, used_at: null } } +// ); + +// // Guarda token +// await PasswordResetToken.create({ +// user_id: user.id, +// token_hash: tokenHash, +// expires_at: expires, +// }); + + +// // Construye URL de reset +// const resetUrl = `${process.env.FRONTEND_URL ?? "http://localhost:5173"}/reset-password?token=${rawToken}`; + +// // Manda email (o log en dev) +// await sendResetEmail(user.email, resetUrl); + +// res.status(200).json(generic); +// }; + +// export const resetPassword = async (req: Request, res: Response): Promise => { +// const { token, newPassword } = req.body as { token: string; newPassword: string }; + +// const tokenHash = hashToken(token); +// const now = new Date(); + +// // Busca token válido +// const prt = await PasswordResetToken.findOne({ +// where: { +// token_hash: tokenHash, +// used_at: null, +// }, +// }); + +// if (!prt || prt.expires_at < now) { +// res.status(400).json({ message: "Token inválido o expirado" }); +// return; +// } + +// // Busca usuario +// const user = await User.findByPk(prt.user_id); +// if (!user) { +// res.status(400).json({ message: "Token inválido" }); +// return; +// } + +// // Actualiza contraseña +// const hash = await bcrypt.hash(newPassword, 10); +// user.password = hash; +// await user.save(); + +// // Marca token como usado +// prt.used_at = new Date(); +// await prt.save(); + +// res.status(200).json({ message: "Contraseña restablecida correctamente. Ya puedes iniciar sesión." }); +// }; import { Request, Response } from "express"; import bcrypt from "bcryptjs"; import User from "../models/UserModel.js"; import PasswordResetToken from "../models/PasswordResetToken.js"; import { generateRawToken, hashToken } from "../utils/resetToken.js"; + // Tipo local en snake_case para trabajar con objetos planos de Sequelize + type PasswordResetTokenSnake = { + id: number | string | bigint; + token_hash: string; + user_id: number | string | bigint; + expires_at: string | Date; + used_at: string | Date | null; +}; + // Enviar email (usa nodemailer en prod; aquí vamos a loguear el link en dev) async function sendResetEmail(to: string, url: string) { if (process.env.NODE_ENV === "production") { // TODO: integra nodemailer o tu proveedor (Sendgrid, SES, etc.) } else { - console.log("🔗 Reset link (dev):", url); + console.log("🔗 Reset link (dev):", url, "➡️ to:", to); } } @@ -34,21 +136,21 @@ export const forgotPassword = async (req: Request, res: Response): Promise // Opcional: invalida tokens previos no usados await PasswordResetToken.update( { used_at: new Date() }, - { where: { user_id: user.id, used_at: null } } + { where: { user_id: (user as any).id, used_at: null } } // forzamos tipo laxo por si id es bigint ); // Guarda token await PasswordResetToken.create({ - user_id: user.id, + user_id: (user as any).id, // evitar choque bigint/number token_hash: tokenHash, expires_at: expires, }); // Construye URL de reset - const resetUrl = `${process.env.FRONTEND_URL ?? "http://localhost:5173"}/reset-password?token=${rawToken}`; + const resetUrl = `${process.env.FRONTEND_URL ?? "http://localhost:5174"}/reset-password?token=${rawToken}`; // Manda email (o log en dev) - await sendResetEmail(user.email, resetUrl); + await sendResetEmail((user as any).email, resetUrl); // tipado laxo por consistencia res.status(200).json(generic); }; @@ -60,33 +162,45 @@ export const resetPassword = async (req: Request, res: Response): Promise const now = new Date(); // Busca token válido - const prt = await PasswordResetToken.findOne({ + const prtRow = await PasswordResetToken.findOne({ where: { token_hash: tokenHash, used_at: null, }, }); - if (!prt || prt.expires_at < now) { + // Si no existe, error inmediato + if (!prtRow) { res.status(400).json({ message: "Token inválido o expirado" }); return; } - // Busca usuario - const user = await User.findByPk(prt.user_id); - if (!user) { + // Convertimos a objeto plano y lo tipamos en snake_case + const t = ((prtRow as any).toJSON ? (prtRow as any).toJSON() : prtRow) as PasswordResetTokenSnake; + + // Aseguramos comparación con Date aunque venga string + if (new Date(t.expires_at) < now) { + res.status(400).json({ message: "Token inválido o expirado" }); + return; + } + + // Busca usuario por PK usando el user_id del token + const userByToken = await User.findByPk(t.user_id as any); + if (!userByToken) { res.status(400).json({ message: "Token inválido" }); return; } // Actualiza contraseña const hash = await bcrypt.hash(newPassword, 10); - user.password = hash; - await user.save(); + (userByToken as any).password = hash; + await userByToken.save(); - // Marca token como usado - prt.used_at = new Date(); - await prt.save(); + // Marca token como usado (update por WHERE, más robusto) + await PasswordResetToken.update( + { used_at: new Date() }, + { where: { token_hash: tokenHash } } + ); res.status(200).json({ message: "Contraseña restablecida correctamente. Ya puedes iniciar sesión." }); }; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index a27e307..60f98e5 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -1,7 +1,10 @@ -import jwt from "jsonwebtoken"; + import jwt from "jsonwebtoken"; -const JWT_SECRET = process.env.JWT_SECRET || "default_secret_change_in_production"; -const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d"; + +// const JWT_SECRET = process.env.JWT_SECRET || "default_secret_change_in_production"; +// const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d"; +const JWT_SECRET = (process.env.JWT_SECRET ?? "default_secret_change_in_production"); +const JWT_EXPIRES_IN = (process.env.JWT_EXPIRES_IN ?? "7d"); export interface TokenPayload { userId: bigint; @@ -19,12 +22,20 @@ export const generateToken = (payload: TokenPayload): string => { email: payload.email, role: payload.role, }; + // const options: SignOptions = { expiresIn: JWT_EXPIRES_IN }; + + + // return jwt.sign(sanitizedPayload, JWT_SECRET, { + // expiresIn: JWT_EXPIRES_IN, + const options: jwt.SignOptions = { + expiresIn: JWT_EXPIRES_IN as unknown as jwt.SignOptions["expiresIn"], + }; - return jwt.sign(sanitizedPayload, JWT_SECRET, { - expiresIn: JWT_EXPIRES_IN, - }); + return jwt.sign(sanitizedPayload, JWT_SECRET as jwt.Secret, options); }; + + /** * Verifica y decodifica un token JWT */ diff --git a/tsconfig.json b/tsconfig.json index 69d2b57..7cfe2d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,8 @@ "outDir": "./dist", // El JS compilado saldrá en dist /* --- Módulos/Entorno (Node ESM) --- */ - "module": "nodenext", // ESM nativo de Node - "moduleResolution": "nodenext", // Resolver imports al estilo Node ESM + "module": "Nodenext", // ESM nativo de Node + "moduleResolution": "Nodenext", // Resolver imports al estilo Node ESM "target": "ES2022", // JS moderno compatible con Node 22 "lib": ["ES2022"], // Librerías base "types": ["node", "jest"], From 42c62bb9552d2892a2eae327814a4bca9bb23968 Mon Sep 17 00:00:00 2001 From: gemayc Date: Sun, 12 Oct 2025 21:17:51 +0200 Subject: [PATCH 40/55] chore(docker): initial Docker image push to Docker Hub from VS Code --- docker-compose.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b0d22ac..de737cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,9 +18,10 @@ services: retries: 10 api: - build: - context: . - dockerfile: Dockerfile + # build: + # context: . + # dockerfile: Dockerfile // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: + image: gema284/codigo-abisal-server-api:dev container_name: abisal-api env_file: .env.docker environment: From eb5d98fb981bb54458cb11e9f36bdee9c6b1753b Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 13 Oct 2025 10:44:52 +0200 Subject: [PATCH 41/55] ci: build & push docker image on develop/main --- .github/workflows/docker-publish.yml | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..3fe2560 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,52 @@ +name: Build & Push Docker image + +on: + push: + branches: [ "develop", "main" ] # se ejecuta cuando hay push a develop o main + workflow_dispatch: # permite lanzarlo a mano desde Actions + +jobs: + docker: + runs-on: ubuntu-latest # VM Ubuntu donde corren los pasos + + steps: + - name: Checkout + uses: actions/checkout@v4 # baja el código del repo (lee el Dockerfile) + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # Buildx = builder moderno de Docker (mejor caché) + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} # <- secret con "gema284" + password: ${{ secrets.DOCKERHUB_TOKEN }} # <- secret con tu access token + + - name: Choose tags + id: vars + shell: bash + run: | + REPO="${{ secrets.DOCKERHUB_USERNAME }}/codigo-abisal-server-api" + SHA="${GITHUB_SHA::7}" # commit corto (tag inmutable) + BR="${GITHUB_REF_NAME}" # nombre de la rama (develop o main) + + # develop -> :dev y :SHA + TAGS="$REPO:dev,$REPO:$SHA" + + # main -> :main y :SHA + if [ "$BR" = "main" ]; then + TAGS="$REPO:main,$REPO:$SHA" + fi + + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + + - name: Build & Push + uses: docker/build-push-action@v6 + with: + context: . # carpeta con tu Dockerfile (raíz) + file: ./Dockerfile # ruta al Dockerfile + push: true # construye y SUBE la imagen a Docker Hub + tags: ${{ steps.vars.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max From 73bb1eca2fee3d65f76b3c7a61bbea23e0c1931c Mon Sep 17 00:00:00 2001 From: olgararo Date: Mon, 13 Oct 2025 11:27:40 +0200 Subject: [PATCH 42/55] feat: new user controller for admin --- src/controllers/UserController.ts | 191 ++++++++++++++++++++++++++++++ src/routes/userRoutes.ts | 16 +++ 2 files changed, 207 insertions(+) create mode 100644 src/controllers/UserController.ts diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts new file mode 100644 index 0000000..896e4e1 --- /dev/null +++ b/src/controllers/UserController.ts @@ -0,0 +1,191 @@ +import { Request, Response } from "express"; +import UserModel from "../models/UserModel.js"; + +/** + * 🎓 EXPLICACIÓN: UserController + * + * Controlador simple para gestión de usuarios por parte del admin. + * Sigue la misma estructura que ArticleController y AuthController. + * + * IMPORTANTE: Estos endpoints requieren middleware isAdmin + */ + +// ======================================== +// 📋 GET /users - Obtener todos los usuarios +// ======================================== +export const getAllUsers = async (_req: Request, res: Response): Promise => { + try { + // Traemos todos los usuarios pero SIN el campo password + const users = await UserModel.findAll({ + attributes: { exclude: ['password'] } // ✅ Excluye password del resultado + }); + + if (!users || users.length === 0) { + res.status(404).json({ message: 'No se encontraron usuarios' }); + return; + } + + res.status(200).json(users); + } catch (error) { + console.error('Error obteniendo usuarios:', error); + res.status(500).json({ + message: 'Error obteniendo usuarios', + error + }); + } +}; + +// ======================================== +// 🗑️ DELETE /user/:id - Eliminar usuario +// ======================================== +export const deleteUser = async (req: Request, res: Response): Promise => { + try { + const { id } = req.params; + + // ✅ VALIDACIÓN 1: Prevenir auto-eliminación + // req.user viene del middleware verifyToken + if (req.user && Number(req.user.userId) === Number(id)) { // (Convierte ambos a number) + res.status(403).json({ + message: "No puedes eliminar tu propia cuenta" + }); + return; + } + + // ✅ VALIDACIÓN 2: Verificar que el usuario existe + const user = await UserModel.findByPk(id); + + if (!user) { + res.status(404).json({ message: "Usuario no encontrado" }); + return; + } + + // ✅ Eliminar el usuario + const deleted = await UserModel.destroy({ + where: { id } + }); + + if (deleted === 0) { + res.status(404).json({ message: "No se pudo eliminar el usuario" }); + return; + } + + res.status(200).json({ + message: "Usuario eliminado correctamente", + deletedUserId: Number(id), + deletedUsername: user.username + }); + } catch (error) { + console.error('Error eliminando usuario:', error); + res.status(500).json({ + message: "No se pudo eliminar el usuario", + error + }); + } +}; + +// ======================================== +// ✏️ PUT /user/:id - Actualizar usuario +// ======================================== + +// Tipo para los campos que pueden actualizarse +interface UpdateUserDTO { + username?: string; + name?: string; + last_name?: string; + email?: string; + role?: string; +} + +export const updateUser = async ( + req: Request<{ id: string }, unknown, UpdateUserDTO>, + res: Response +): Promise => { + try { + const { id } = req.params; + + // ✅ VALIDACIÓN 1: Verificar que no se intente actualizar password + if ('password' in req.body) { + res.status(400).json({ + message: 'No puedes actualizar la contraseña desde este endpoint. Usa /auth/reset-password' + }); + return; + } + + // ✅ VALIDACIÓN 2: Verificar que el usuario existe + const user = await UserModel.findByPk(id); + + if (!user) { + res.status(404).json({ message: "Usuario no encontrado" }); + return; + } + + // ✅ VALIDACIÓN 3: Si se cambia el rol, verificar que sea válido + const { role, email, username, name, last_name } = req.body; + + if (role && !['user', 'admin'].includes(role)) { + res.status(400).json({ + message: 'El rol debe ser "user" o "admin"' + }); + return; + } + + // ✅ VALIDACIÓN 4: Si se cambia el email, verificar que no exista + if (email) { + const normalizedEmail = email.toLowerCase().trim(); + + const existingUser = await UserModel.findOne({ + where: { email: normalizedEmail } + }); + + // Verificar que el email no pertenezca a otro usuario + if (existingUser && existingUser.id !== user.id) { + res.status(409).json({ + message: "Este email ya está en uso por otro usuario" + }); + return; + } + } + + // ✅ VALIDACIÓN 5: Si se cambia el username, verificar que no exista + if (username) { + const normalizedUsername = username.toLowerCase().trim(); + + const existingUsername = await UserModel.findOne({ + where: { username: normalizedUsername } + }); + + // Verificar que el username no pertenezca a otro usuario + if (existingUsername && existingUsername.id !== user.id) { + res.status(409).json({ + message: "Este nombre de usuario ya está en uso" + }); + return; + } + } + + // ✅ Actualizar el usuario (solo los campos enviados) + await user.update({ + username, + name, + last_name, + email: email ? email.toLowerCase().trim() : undefined, + role + }); + + // ✅ Devolver el usuario actualizado sin password + const updatedUser = await UserModel.findByPk(id, { + attributes: { exclude: ['password'] } + }); + + res.status(200).json({ + message: "Usuario actualizado correctamente", + user: updatedUser, + }); + } catch (error) { + console.error('Error actualizando usuario:', error); + res.status(500).json({ + message: "Error actualizando el usuario", + error + }); + } +}; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 41c3913..cb744cd 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,5 +1,7 @@ import { Router, Request, Response } from "express"; import User from "../models/UserModel.js" +import {getAllUsers, deleteUser, updateUser} from "../controllers/UserController.js"; +import { verifyToken, isAdmin } from "../middlewares/authMiddlewares.js"; const router = Router(); @@ -34,4 +36,18 @@ router.get("/user/:id", async (req: Request, res: Response) => { } }); +router.use(verifyToken); +router.use(isAdmin); + +// 🔒 Estas rutas requieren verifyToken + isAdmin + +// 🗑️ DELETE /user/:id - Eliminar usuario +router.delete("/:id", verifyToken, isAdmin, deleteUser); + +// ✏️ PUT /user/:id - Actualizar usuario +router.put("/:id", verifyToken, isAdmin, updateUser); + +// 📋 GET /users - Obtener todos los usuarios +router.get("/", verifyToken, isAdmin, getAllUsers); + export default router; From d8c3e2beb6a7858983542129a0a8c8985a8b5c82 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Mon, 13 Oct 2025 15:02:23 +0200 Subject: [PATCH 43/55] feat(articles): add like and unlike functionality --- src/app.ts | 2 +- src/controllers/ArticleController.ts | 62 ++++++++++++++++++++++ src/controllers/PasswordResetController.ts | 3 +- src/database/db_connection.ts | 8 ++- src/models/ArticleModel.ts | 23 ++++++++ src/routes/articleRoutes.ts | 7 ++- test/.gitkeep | 0 7 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 test/.gitkeep diff --git a/src/app.ts b/src/app.ts index 5165165..d173279 100644 --- a/src/app.ts +++ b/src/app.ts @@ -33,7 +33,7 @@ app.use("/auth", passwordResetRouter); async function startServer() { try { // Sincroniza los modelos con la base de datos - await db_connection.sync(); + await db_connection.sync({ alter: true }); console.log("✅ Database synchronized successfully."); app.listen(PORT, () => { diff --git a/src/controllers/ArticleController.ts b/src/controllers/ArticleController.ts index 57f20ba..b86e23d 100644 --- a/src/controllers/ArticleController.ts +++ b/src/controllers/ArticleController.ts @@ -1,5 +1,6 @@ import { Article } from "../models/ArticleModel.js"; import type { Request, Response} from "express"; +import { User } from '../models/UserModel.js'; export const getAllArticles = async (_req: Request, res: Response) => { @@ -136,3 +137,64 @@ export const updateArticle = async ( return res.status(500).json({ message: "Error actualizando el artículo" }); } }; + +export const likeArticle = async (req: Request, res: Response) => { + try { + const articleId = req.params.id; + const userId = req.user?.userId; + + if (!userId) { + return res.status(401).json({ message: 'No estás logueado' }); + } + + const article = await Article.findByPk(articleId); + const user = await User.findByPk(String(userId)); + + // Si el artículo o el usuario no se encontraron, nos detenemos aquí. + if (!article || !user) { + return res.status(404).json({ message: 'Artículo o usuario no encontrado' }); + } + + // --- Si llegamos hasta aquí, TypeScript ya sabe que 'article' SÍ existe --- + + await (article as any).addLikedByUsers(user); + article.likes += 1; // Ahora esta línea es segura + await article.save(); + + res.status(200).json({ message: 'Like añadido', likes: article.likes }); + + } catch (error) { + res.status(500).json({ message: 'Error en el servidor' }); + } +}; + +// Función para QUITAR like +export const unlikeArticle = async (req: Request, res: Response) => { + try { + const articleId = req.params.id; + const userId = req.user?.userId; + + if (!userId) { + return res.status(401).json({ message: 'No estás logueado' }); + } + + const article = await Article.findByPk(articleId); + const user = await User.findByPk(String(userId)); + + // Misma comprobación aquí + if (!article || !user) { + return res.status(404).json({ message: 'Artículo o usuario no encontrado' }); + } + + // --- Igual que antes, TypeScript ya sabe que 'article' existe --- + + await (article as any).removeLikedByUsers(user); + article.likes = Math.max(0, article.likes - 1); // Y esta línea también es segura + await article.save(); + + res.status(200).json({ message: 'Like eliminado', likes: article.likes }); + + } catch (error) { + res.status(500).json({ message: 'Error en el servidor' }); + } +}; diff --git a/src/controllers/PasswordResetController.ts b/src/controllers/PasswordResetController.ts index c4ce324..d6a5b48 100644 --- a/src/controllers/PasswordResetController.ts +++ b/src/controllers/PasswordResetController.ts @@ -39,8 +39,7 @@ // // Guarda token // await PasswordResetToken.create({ -// user_id: user.id, -// token_hash: tokenHash, +// user_id: user.id, // token_hash: tokenHash, // expires_at: expires, // }); diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index a7e84e9..3914d0b 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -9,7 +9,9 @@ const isTest = process.env.NODE_ENV === "test"; // 3) Seguridad: en test, exige que la BD termine en _test if (isTest && process.env.DB_NAME && !process.env.DB_NAME.endsWith("_test")) { - throw new Error(`En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}`); + throw new Error( + `En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}` + ); } // 4) Conexión @@ -20,7 +22,9 @@ const db_connection = new Sequelize( { host: process.env.DB_HOST || "localhost", dialect: "mysql", - logging: isTest ? false : console.log, + logging: console.log, // <-- CAMBIA ESTO + + // logging: isTest ? false : console.log, define: { timestamps: false }, } ); diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts index 439a988..c6f885c 100644 --- a/src/models/ArticleModel.ts +++ b/src/models/ArticleModel.ts @@ -3,6 +3,8 @@ import { DataTypes, Model, Optional } from "sequelize"; // 2) Importo mi conexión a la base de datos (tu Sequelize ya configurado) import db_connection from "../database/db_connection.js"; import { ArticleAttributes } from "../interface/articleInterface.js"; +import { User } from './UserModel.js'; + // 4) Campos opcionales AL CREAR (Sequelize los rellena solo) export type ArticleCreationAttributes = Optional< @@ -26,6 +28,7 @@ export class Article declare references: string; declare created_at: Date; declare updated_at: Date; + declare likes?: number; } // 6) Inicializo (equivalente a define) y mapeo columnas/validaciones @@ -111,6 +114,11 @@ Article.init( allowNull: false, defaultValue: DataTypes.NOW, }, + likes: { // <-- AÑADIR ESTE CAMPO + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }, }, { sequelize: db_connection, // ← tu conexión @@ -122,5 +130,20 @@ Article.init( } ); +export const UserLikes = db_connection.define('user_likes', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, +}, { + timestamps: false, + tableName: 'user_likes', +}); + +User.belongsToMany(Article, { through: UserLikes, as: 'likedArticles' }); +Article.belongsToMany(User, { through: UserLikes, as: 'likedByUsers' }); + + // 7) ¡Exporto el modelo! (puedes default o nombrado) export default Article; diff --git a/src/routes/articleRoutes.ts b/src/routes/articleRoutes.ts index 231aab7..739b9b6 100644 --- a/src/routes/articleRoutes.ts +++ b/src/routes/articleRoutes.ts @@ -1,5 +1,5 @@ import express from 'express'; -import {getAllArticles,getArticleById, deleteArticle,createArticle,updateArticle} from '../controllers/ArticleController.js'; +import {getAllArticles,getArticleById, deleteArticle,createArticle,updateArticle, likeArticle, unlikeArticle } from '../controllers/ArticleController.js'; import { authMiddleware, requireRole, handleValidation} from '../middlewares/authMiddlewares.js'; import { createArticleValidators, updateArticleValidators, idParamValidators } from '../validators/articleValidators.js'; // import { checkValidations } from "../middlewares/articleMiddlewares.js"; @@ -16,6 +16,11 @@ articleRouter.get("/:id", idParamValidators, getArticleById,); articleRouter.post("/", authMiddleware, requireRole(["admin", "user"]), createArticleValidators, handleValidation, createArticle,); articleRouter.put("/:id", authMiddleware, requireRole(["admin"]), idParamValidators, updateArticleValidators, handleValidation, updateArticle,); articleRouter.delete("/:id",authMiddleware, requireRole(["admin"]), idParamValidators, handleValidation, deleteArticle); +// Dar like +articleRouter.post("/:id/like", authMiddleware, idParamValidators, handleValidation, likeArticle); + +// Quitar like +articleRouter.delete("/:id/like", authMiddleware, idParamValidators, handleValidation, unlikeArticle); export default articleRouter; \ No newline at end of file diff --git a/test/.gitkeep b/test/.gitkeep new file mode 100644 index 0000000..e69de29 From 95706aedefcc2ab848439586680b338380384aa4 Mon Sep 17 00:00:00 2001 From: Camila Arenas Date: Mon, 13 Oct 2025 15:06:18 +0200 Subject: [PATCH 44/55] remove alter true form app.ts --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index d173279..5165165 100644 --- a/src/app.ts +++ b/src/app.ts @@ -33,7 +33,7 @@ app.use("/auth", passwordResetRouter); async function startServer() { try { // Sincroniza los modelos con la base de datos - await db_connection.sync({ alter: true }); + await db_connection.sync(); console.log("✅ Database synchronized successfully."); app.listen(PORT, () => { From 126ffa7156dbe757109c141a0e3e15449786a649 Mon Sep 17 00:00:00 2001 From: olgararo Date: Mon, 13 Oct 2025 15:09:42 +0200 Subject: [PATCH 45/55] fix: user routes imports for new endpoints --- src/routes/userRoutes.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index cb744cd..3224a47 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,7 +1,7 @@ import { Router, Request, Response } from "express"; import User from "../models/UserModel.js" import {getAllUsers, deleteUser, updateUser} from "../controllers/UserController.js"; -import { verifyToken, isAdmin } from "../middlewares/authMiddlewares.js"; +import { authMiddleware, requireRole } from "../middlewares/authMiddlewares.js"; const router = Router(); @@ -36,18 +36,14 @@ router.get("/user/:id", async (req: Request, res: Response) => { } }); -router.use(verifyToken); -router.use(isAdmin); -// 🔒 Estas rutas requieren verifyToken + isAdmin +// 📋 GET /users - Obtener todos los usuarios +router.get("/", authMiddleware, requireRole(["admin"]), getAllUsers); // 🗑️ DELETE /user/:id - Eliminar usuario -router.delete("/:id", verifyToken, isAdmin, deleteUser); +router.delete("/:id", authMiddleware, requireRole(["admin"]), deleteUser); // ✏️ PUT /user/:id - Actualizar usuario -router.put("/:id", verifyToken, isAdmin, updateUser); - -// 📋 GET /users - Obtener todos los usuarios -router.get("/", verifyToken, isAdmin, getAllUsers); +router.put("/:id", authMiddleware, requireRole(["admin"]), updateUser); export default router; From ed96675d25b3c87a69cdfa0e95aea50665e15e8e Mon Sep 17 00:00:00 2001 From: olgararo Date: Mon, 13 Oct 2025 16:28:19 +0200 Subject: [PATCH 46/55] fix(app.ts): add /users for routes --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 5165165..13e6dd1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -25,7 +25,7 @@ Article.belongsTo(User, { foreignKey: 'creator_id' }); }); app.use("/auth", authRouter ) app.use("/article", articleRouter) -app.use(userRouter); // /user/:id +app.use("/users", userRouter); app.use("/auth", passwordResetRouter); // await db_connection.sync({ alter: true }); // o { force: true } si quieres regenerar From 63cd24ce22654df3ab4f95b1bffa03a4378b7607 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 13 Oct 2025 17:03:39 +0200 Subject: [PATCH 47/55] feat(docker): TiDB TLS + compose para nube; ignorar dumps/certs y actualizar .env.example --- .env.example | 36 ++++++++++++++++++++---- .gitignore | 11 +++++++- docker-compose.yml | 53 ++++++++++++++++++----------------- src/database/db_connection.ts | 51 +++++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index 7462df1..335c209 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,36 @@ -DB_NAME= -DB_USER= -DB_PASS= +# .env que utliza la base de datos local MYSQL + +DB_NAME= +DB_USER= +DB_PASS= DB_HOST= +DB_PORT= DB_DIALECT= +DB_SSL=false + PORT= CORS_ORIGIN= -JWT_SECRET= -JWT_EXPIRES = \ No newline at end of file +JWT_SECRET= +JWT_EXPIRES= + +# .env.docker que utiliza una base de datos en la nuebe TIBD y que solo utilizamos en el docker-compose.yml + +NODE_ENV=development + +DB_NAME= +DB_USER= +DB_PASS= +DB_HOST= +DB_PORT= + +DB_DIALECT= + +DB_SSL=true +DB_SSL_CA_PATH= + + +PORT= +CORS_ORIGIN= +JWT_SECRET= +JWT_EXPIRES= diff --git a/.gitignore b/.gitignore index 6008da1..4e0eef9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,13 @@ node_modules .env.test .DS_Store dist -.env.docker \ No newline at end of file +.env.docker + +# Certificados / claves +certs/*.pem +certs/*.key + +# Dumps y backups +*.sql +*.sql.gz +*.dump \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index de737cf..f1a6d0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,39 @@ services: - mysql: - image: mysql:8.4 - container_name: mysql-abisal - environment: - - MYSQL_ROOT_PASSWORD=alba2005 # contraseña del root - - MYSQL_DATABASE=abisal_app # base de datos que se crea al iniciar - - MYSQL_USER=app # usuario normal (NO root) - - MYSQL_PASSWORD=alba2005 # contraseña app - ports: - - "3307:3306" # mapeo externo (puedes dejarlo o quitarlo) - volumes: - - mysql_data:/var/lib/mysql - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 10s - timeout: 5s - retries: 10 + # mysql: + # image: mysql:8.4 + # container_name: mysql-abisal + # environment: + # - MYSQL_ROOT_PASSWORD=alba2005 # contraseña del root + # - MYSQL_DATABASE=abisal_app # base de datos que se crea al iniciar + # - MYSQL_USER=app # usuario normal (NO root) + # - MYSQL_PASSWORD=alba2005 # contraseña app + # ports: + # - "3307:3306" # mapeo externo (puedes dejarlo o quitarlo) + # volumes: + # - mysql_data:/var/lib/mysql + # healthcheck: + # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + # interval: 10s + # timeout: 5s + # retries: 10 api: # build: # context: . # dockerfile: Dockerfile // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: - image: gema284/codigo-abisal-server-api:dev container_name: abisal-api + image: gema284/codigo-abisal-server-api:dev env_file: .env.docker - environment: - - DB_HOST=mysql + # environment: + # - DB_HOST=mysql ports: - "8000:8000" - depends_on: - mysql: - condition: service_healthy + # depends_on: + # mysql: + # condition: service_healthy + volumes: + - ./certs/tidb-ca.pem:/app/certs/tidb-ca.pem:ro + +# volumes: -volumes: - mysql_data: +# mysql_data: diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index a7e84e9..1e36a90 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -1,7 +1,38 @@ +// import { Sequelize } from "sequelize"; +// import dotenv from "dotenv"; + +// // 1) Cargar .env.test si estamos en test; si no, .env normal +// dotenv.config({ path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" }); + +// // 2) Bandera simple para saber si estamos en test +// const isTest = process.env.NODE_ENV === "test"; + +// // 3) Seguridad: en test, exige que la BD termine en _test +// if (isTest && process.env.DB_NAME && !process.env.DB_NAME.endsWith("_test")) { +// throw new Error(`En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}`); +// } + +// // 4) Conexión +// const db_connection = new Sequelize( +// process.env.DB_NAME as string, +// process.env.DB_USER as string, +// process.env.DB_PASS as string, +// { +// host: process.env.DB_HOST || "localhost", +// dialect: "mysql", +// logging: isTest ? false : console.log, +// define: { timestamps: false }, +// } +// ); + +// export default db_connection; + +// src/database/db_connection.ts import { Sequelize } from "sequelize"; +import fs from "fs"; import dotenv from "dotenv"; -// 1) Cargar .env.test si estamos en test; si no, .env normal +// 1) Cargar .env.test si estamos en test; si no, .env normal (opcional si ya te inyecta Docker) dotenv.config({ path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" }); // 2) Bandera simple para saber si estamos en test @@ -12,14 +43,30 @@ if (isTest && process.env.DB_NAME && !process.env.DB_NAME.endsWith("_test")) { throw new Error(`En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}`); } -// 4) Conexión +// 4) TLS opcional +const sslEnabled = String(process.env.DB_SSL || "").toLowerCase() === "true"; + +let dialectOptions: any = {}; +if (sslEnabled) { + const caPath = process.env.DB_SSL_CA_PATH; // p.ej. /app/certs/tidb-ca.pem en Docker + const hasCA = caPath && fs.existsSync(caPath); + const ca = hasCA ? fs.readFileSync(caPath, "utf8") : undefined; + + dialectOptions = ca + ? { ssl: { ca, minVersion: "TLSv1.2", rejectUnauthorized: true } } + : { ssl: { minVersion: "TLSv1.2", rejectUnauthorized: true } }; +} + +// 5) Conexión Sequelize const db_connection = new Sequelize( process.env.DB_NAME as string, process.env.DB_USER as string, process.env.DB_PASS as string, { host: process.env.DB_HOST || "localhost", + port: Number(process.env.DB_PORT) || 3306, // local 3306; en TiDB pon 4000 por env dialect: "mysql", + dialectOptions, // activa TLS si DB_SSL=true logging: isTest ? false : console.log, define: { timestamps: false }, } From c030fea59c7ddc1f5a1518b2aab796b707553375 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 13 Oct 2025 17:12:20 +0200 Subject: [PATCH 48/55] chore: delete accidental backend.sql and ignore *.sql --- backup.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backup.sql diff --git a/backup.sql b/backup.sql deleted file mode 100644 index e69de29..0000000 From 20075ded2fd57629913d4d2edd0ae552192d554a Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 13 Oct 2025 17:21:29 +0200 Subject: [PATCH 49/55] chore(docker): add comments to docker-compose.yml for clarity --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f1a6d0c..0d966f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: - # mysql: + + # mysql: // todo esto lo he comentado porque esta es la BD MYSQL que yo tenia en local ahora ya no la necesito la he echo en la nube # image: mysql:8.4 # container_name: mysql-abisal # environment: From 9e30e0048978f828058081a3096f3110895bc440 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 13 Oct 2025 17:49:31 +0200 Subject: [PATCH 50/55] feat(likes): align Like interface with model and associations Sync field names (user_id, article_id) and ensure compatibility with Sequelize and relationships. --- docker-compose.yml | 8 ++++---- src/interface/articleInterface.ts | 2 +- src/models/ArticleModel.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0d966f6..412861a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,11 +19,11 @@ services: # retries: 10 api: - # build: - # context: . - # dockerfile: Dockerfile // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: + build: + context: . + dockerfile: Dockerfile container_name: abisal-api - image: gema284/codigo-abisal-server-api:dev + # image: gema284/codigo-abisal-server-api:dev // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: env_file: .env.docker # environment: # - DB_HOST=mysql diff --git a/src/interface/articleInterface.ts b/src/interface/articleInterface.ts index 721efad..9aaec2c 100644 --- a/src/interface/articleInterface.ts +++ b/src/interface/articleInterface.ts @@ -8,7 +8,7 @@ export interface ArticleAttributes { species: string; image?: string; references?: string; -// likes_count: bigint; + likes: number; created_at: Date; updated_at: Date; } \ No newline at end of file diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts index c6f885c..1eb297f 100644 --- a/src/models/ArticleModel.ts +++ b/src/models/ArticleModel.ts @@ -28,7 +28,7 @@ export class Article declare references: string; declare created_at: Date; declare updated_at: Date; - declare likes?: number; + declare likes: number; } // 6) Inicializo (equivalente a define) y mapeo columnas/validaciones From b4f8846a30e05cdaa64bd66aa97f7ea70ef69ff9 Mon Sep 17 00:00:00 2001 From: gemayc Date: Mon, 13 Oct 2025 18:57:23 +0200 Subject: [PATCH 51/55] feat(infra): Docker + TiDB TLS y columna 'likes' (imagen en compose, CI a Docker Hub) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Conexión MySQL con TLS opcional (DB_SSL, DB_PORT=4000, DB_SSL_CA_PATH) para TiDB Cloud. • Compose usa imagen de Docker Hub (gema284/codigo-abisal-server-api:dev) y monta CA. • Columna 'likes' en Article (DEFAULT 0); tipado ajustado y creación segura. • DB_AUTO_ALTER: true solo en local, false en nube (no toca esquema en prod). • .env.example actualizado; .gitignore ignora .env*, certs/*.pem y *.sql. • (CI) Workflow para build & push de imagen a Docker Hub al push/PR en develop/main. --- docker-compose.yml | 8 ++++---- src/models/ArticleModel.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 412861a..0d966f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,11 +19,11 @@ services: # retries: 10 api: - build: - context: . - dockerfile: Dockerfile + # build: + # context: . + # dockerfile: Dockerfile // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: container_name: abisal-api - # image: gema284/codigo-abisal-server-api:dev // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: + image: gema284/codigo-abisal-server-api:dev env_file: .env.docker # environment: # - DB_HOST=mysql diff --git a/src/models/ArticleModel.ts b/src/models/ArticleModel.ts index 1eb297f..65d2b9d 100644 --- a/src/models/ArticleModel.ts +++ b/src/models/ArticleModel.ts @@ -9,7 +9,7 @@ import { User } from './UserModel.js'; // 4) Campos opcionales AL CREAR (Sequelize los rellena solo) export type ArticleCreationAttributes = Optional< ArticleAttributes, - "id" | "created_at" | "updated_at" | "image" | "references" + "id" | "created_at" | "updated_at" | "image" | "references" | "likes" >; // 5) Defino la clase del modelo (tipada) From 4ccf476cd71e8608787c736687aa4e6edf57c5f6 Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 14 Oct 2025 15:30:30 +0200 Subject: [PATCH 52/55] change in appfix(app): resolve issue in app.ts related to initialization logic --- src/app.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 13e6dd1..dab12bb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,12 +17,15 @@ User.hasMany(Article, { foreignKey: 'creator_id' }); Article.belongsTo(User, { foreignKey: 'creator_id' }); export const app = express(); - const PORT = process.env.PORT || 8000; +const PORT = process.env.PORT ? Number(process.env.PORT) : 8000; app.use(cors({ origin: process.env.CORS_ORIGIN || "*" })); // puerto de Vite app.use(express.json()); app.get("/", (_req, res) => { res.send("Hola API"); }); +app.get("/healthz", (_req, res) => { + res.status(200).send("ok"); +}); app.use("/auth", authRouter ) app.use("/article", articleRouter) app.use("/users", userRouter); From 98024f6ad8f4a898b8cae7fab3a3ce5b5656ba92 Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 14 Oct 2025 15:56:05 +0200 Subject: [PATCH 53/55] chore(db, docker): update database connection settings and improve Dockerfile configuration --- Dockerfile | 3 +++ src/database/db_connection.ts | 29 ++++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index cdf28bc..387d0e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ COPY --from=deps /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY package*.json ./ +# Copiamos el certificado de TiDB dentro de la imagen +COPY certs ./certs + # Seguridad: no correr como root RUN addgroup -S app && adduser -S app -G app RUN chown -R app:app /app diff --git a/src/database/db_connection.ts b/src/database/db_connection.ts index d38fb16..f6dd45f 100644 --- a/src/database/db_connection.ts +++ b/src/database/db_connection.ts @@ -32,43 +32,46 @@ import { Sequelize } from "sequelize"; import fs from "fs"; import dotenv from "dotenv"; -// 1) Cargar .env.test si estamos en test; si no, .env normal (opcional si ya te inyecta Docker) +// (1) Cargar .env.test si estamos en test; si no, .env normal dotenv.config({ path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" }); -// 2) Bandera simple para saber si estamos en test +// (2) Saber si estamos en test const isTest = process.env.NODE_ENV === "test"; -// 3) Seguridad: en test, exige que la BD termine en _test +// (3) En test, obligar a que la BD termine en _test (seguridad) if (isTest && process.env.DB_NAME && !process.env.DB_NAME.endsWith("_test")) { - throw new Error( - `En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}` - ); + throw new Error(`En test, DB_NAME debe terminar en "_test". Actual: ${process.env.DB_NAME}`); } -// 4) TLS opcional +// (4) TLS opcional activado por env const sslEnabled = String(process.env.DB_SSL || "").toLowerCase() === "true"; +// (5) Preparar opciones de SSL para Sequelize let dialectOptions: any = {}; if (sslEnabled) { - const caPath = process.env.DB_SSL_CA_PATH; // p.ej. /app/certs/tidb-ca.pem en Docker - const hasCA = caPath && fs.existsSync(caPath); - const ca = hasCA ? fs.readFileSync(caPath, "utf8") : undefined; + const caPath = process.env.DB_SSL_CA_PATH; // p.ej. /app/certs/tidb-ca.pem en Docker/Render + const hasFile = caPath && fs.existsSync(caPath); + const caFromFile = hasFile ? fs.readFileSync(caPath, "utf8") : undefined; + + const caFromEnv = process.env.DB_SSL_CA_PEM; // alternativa: el PEM entero en una ENV + + const ca = caFromEnv || caFromFile; // preferimos ENV si está; si no, archivo dialectOptions = ca ? { ssl: { ca, minVersion: "TLSv1.2", rejectUnauthorized: true } } : { ssl: { minVersion: "TLSv1.2", rejectUnauthorized: true } }; } -// 5) Conexión Sequelize +// (6) Conexión Sequelize const db_connection = new Sequelize( process.env.DB_NAME as string, process.env.DB_USER as string, process.env.DB_PASS as string, { host: process.env.DB_HOST || "localhost", - port: Number(process.env.DB_PORT) || 3306, // local 3306; en TiDB pon 4000 por env + port: Number(process.env.DB_PORT) || 3306, // TiDB: 4000 dialect: "mysql", - dialectOptions, // activa TLS si DB_SSL=true + dialectOptions, // activa TLS si DB_SSL=true logging: isTest ? false : console.log, define: { timestamps: false }, } From ca3a4b001899f6c600cfba540548e8ca86bf4e8b Mon Sep 17 00:00:00 2001 From: gemayc Date: Tue, 14 Oct 2025 21:38:14 +0200 Subject: [PATCH 54/55] chore(compose): update docker-compose for testing purposes --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0d966f6..e26e911 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,9 +32,9 @@ services: # depends_on: # mysql: # condition: service_healthy - volumes: - - ./certs/tidb-ca.pem:/app/certs/tidb-ca.pem:ro + # volumes: + # - ./certs/tidb-ca.pem:/app/certs/tidb-ca.pem:ro//este lo que quitado ahora porque lo he metido en la imagen dockerfile # volumes: -# mysql_data: +# mysql_data:// este lo quito porque apuntaba a la BD local de docker From 83ff3994c208d4c1a92f3b3e3e87191e061e6c31 Mon Sep 17 00:00:00 2001 From: gemayc Date: Wed, 15 Oct 2025 16:07:45 +0200 Subject: [PATCH 55/55] update docker-compose --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index e26e911..c5c6128 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: # context: . # dockerfile: Dockerfile // cuando se construye la imagen desde local ya no lo necesito la tengo en dockerhub ahora pongo: container_name: abisal-api + platform: linux/amd64 image: gema284/codigo-abisal-server-api:dev env_file: .env.docker # environment: