feat: add JWT authentication
This commit is contained in:
170
backend/package-lock.json
generated
170
backend/package-lock.json
generated
@@ -11,13 +11,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
"@prisma/client": "^7.8.0",
|
"@prisma/client": "^7.8.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"env-var": "^7.5.0",
|
"env-var": "^7.5.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.21.0"
|
"pg": "^8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"prisma": "^7.8.0",
|
"prisma": "^7.8.0",
|
||||||
@@ -882,6 +886,13 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
@@ -935,6 +946,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.9.1",
|
"version": "25.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||||
@@ -1051,6 +1080,15 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/better-result": {
|
"node_modules/better-result": {
|
||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz",
|
||||||
@@ -1095,6 +1133,12 @@
|
|||||||
"node": "18 || 20 || >=22"
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1347,6 +1391,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -1913,6 +1966,91 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"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/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"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": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.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": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
@@ -2584,6 +2722,26 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -2598,6 +2756,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||||
|
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"prisma": "^7.8.0",
|
"prisma": "^7.8.0",
|
||||||
@@ -24,9 +26,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
"@prisma/adapter-pg": "^7.8.0",
|
||||||
"@prisma/client": "^7.8.0",
|
"@prisma/client": "^7.8.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"env-var": "^7.5.0",
|
"env-var": "^7.5.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.21.0"
|
"pg": "^8.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import express from 'express';
|
import { env } from "./config/env.js";
|
||||||
|
import { AppRoutes } from "./presentation/routes.js";
|
||||||
|
import { Server } from "./presentation/server.js";
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const port = process.env.PORT ?? 3000;
|
|
||||||
|
|
||||||
app.use(express.json());
|
async function main() {
|
||||||
|
const server = new Server( {
|
||||||
app.get('/', (_req, res) => {
|
port : env.PORT,
|
||||||
res.json({
|
routes : AppRoutes.routes,
|
||||||
message: 'API funcionando con Express + TypeScript + TSX',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
await server.start();
|
||||||
|
|
||||||
app.listen(port, () => {
|
}
|
||||||
console.log(`Servidor corriendo en http://localhost:${port}`);
|
|
||||||
});
|
main();
|
||||||
9
backend/src/config/env.ts
Normal file
9
backend/src/config/env.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import envVar from 'env-var';
|
||||||
|
|
||||||
|
export const env = {
|
||||||
|
PORT: envVar.get('PORT').required().asPortNumber(),
|
||||||
|
PUBLIC_PATH: envVar.get('PUBLIC_PATH').default('public').asString(),
|
||||||
|
POSTGRES_URL: envVar.get('POSTGRES_URL').required().asString()
|
||||||
|
}
|
||||||
|
|
||||||
20
backend/src/config/jwt.ts
Normal file
20
backend/src/config/jwt.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET ?? 'dev_secret';
|
||||||
|
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN ?? '2h';
|
||||||
|
|
||||||
|
export class JwtAdapter {
|
||||||
|
static generateToken(payload: object): string {
|
||||||
|
return jwt.sign(payload, JWT_SECRET, {
|
||||||
|
expiresIn: JWT_EXPIRES_IN,
|
||||||
|
} as SignOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static validateToken<T>(token: string): T | null {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, JWT_SECRET) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
backend/src/data/postgres/index.ts
Normal file
9
backend/src/data/postgres/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
import { env } from "../../config/env.js";
|
||||||
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { PrismaClient } from "../../generated/prisma/client.js";
|
||||||
|
|
||||||
|
const connectionString = `${env.POSTGRES_URL}`;
|
||||||
|
const adapter = new PrismaPg({ connectionString });
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient({ adapter });
|
||||||
15
backend/src/domain/dtos/auth/login-user.dto.ts
Normal file
15
backend/src/domain/dtos/auth/login-user.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export class LoginUserDto {
|
||||||
|
private constructor(
|
||||||
|
public readonly email: string,
|
||||||
|
public readonly password: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static create(props: { [key: string]: any }): [string | undefined, LoginUserDto?] {
|
||||||
|
const { email, password } = props;
|
||||||
|
|
||||||
|
if (!email) return ['email is required'];
|
||||||
|
if (!password) return ['password is required'];
|
||||||
|
|
||||||
|
return [undefined, new LoginUserDto(email, password)];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
backend/src/domain/dtos/auth/register-user.dto.ts
Normal file
21
backend/src/domain/dtos/auth/register-user.dto.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export class RegisterUserDto {
|
||||||
|
private constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly email: string,
|
||||||
|
public readonly password: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static create(props: { [key: string]: any }): [string | undefined, RegisterUserDto?] {
|
||||||
|
const { name, email, password } = props;
|
||||||
|
|
||||||
|
if (!name) return ['name is required'];
|
||||||
|
if (!email) return ['email is required'];
|
||||||
|
if (!password) return ['password is required'];
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return ['password must be at least 6 characters'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [undefined, new RegisterUserDto(name, email, password)];
|
||||||
|
}
|
||||||
|
}
|
||||||
2
backend/src/domain/dtos/index.ts
Normal file
2
backend/src/domain/dtos/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './auth/register-user.dto.js';
|
||||||
|
export * from './auth/login-user.dto.js';
|
||||||
137
backend/src/presentation/auth/controller.ts
Normal file
137
backend/src/presentation/auth/controller.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
import { prisma } from "../../data/postgres/index.js";
|
||||||
|
import { JwtAdapter } from "../../config/jwt.js";
|
||||||
|
import { LoginUserDto, RegisterUserDto } from "../../domain/dtos/index.js";
|
||||||
|
import { AuthRequest } from "../middlewares/auth.middleware.js";
|
||||||
|
|
||||||
|
export class AuthController {
|
||||||
|
|
||||||
|
// SIN metodos estaticos para hacer DI
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public registerUser = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
|
const [error, registerUserDto] = RegisterUserDto.create(req.body);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userExists = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: registerUserDto!.email,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userExists) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Email already exists'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = bcrypt.hashSync(registerUserDto!.password, 10);
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name: registerUserDto!.name,
|
||||||
|
email: registerUserDto!.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = JwtAdapter.generateToken({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public loginUser = async (req: Request, res: Response) => {
|
||||||
|
|
||||||
|
const [error, loginUserDto] = LoginUserDto.create(req.body);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return res.status(400).json({ error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
email: loginUserDto!.email,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid email or password'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = bcrypt.compareSync(
|
||||||
|
loginUserDto!.password,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid email or password'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = JwtAdapter.generateToken({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMe = async (req: AuthRequest, res: Response) => {
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'User not authenticated'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
user: req.user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
20
backend/src/presentation/auth/routes.ts
Normal file
20
backend/src/presentation/auth/routes.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { AuthController } from "./controller.js";
|
||||||
|
import { AuthMiddleware } from "../middlewares/auth.middleware.js";
|
||||||
|
|
||||||
|
export class AuthRoutes {
|
||||||
|
|
||||||
|
static get routes(): Router {
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const controller = new AuthController();
|
||||||
|
|
||||||
|
router.post('/register', controller.registerUser);
|
||||||
|
router.post('/login', controller.loginUser);
|
||||||
|
|
||||||
|
// Ruta protegida para probar JWT
|
||||||
|
router.get('/me', AuthMiddleware.validateJwt, controller.getMe);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/src/presentation/middlewares/auth.middleware.ts
Normal file
46
backend/src/presentation/middlewares/auth.middleware.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { JwtAdapter } from "../../config/jwt.js";
|
||||||
|
|
||||||
|
interface TokenPayload {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthRequest extends Request {
|
||||||
|
user?: TokenPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthMiddleware {
|
||||||
|
|
||||||
|
static validateJwt = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
|
||||||
|
const authorization = req.header('Authorization');
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'No token provided'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorization.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid Bearer token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authorization.split(' ').at(1) ?? '';
|
||||||
|
|
||||||
|
const payload = JwtAdapter.validateToken<TokenPayload>(token);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = payload;
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/src/presentation/routes.ts
Normal file
14
backend/src/presentation/routes.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { AuthRoutes } from "./auth/routes.js";
|
||||||
|
|
||||||
|
export class AppRoutes {
|
||||||
|
|
||||||
|
static get routes(): Router {
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use('/api/auth', AuthRoutes.routes);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/src/presentation/server.ts
Normal file
46
backend/src/presentation/server.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import express, { Router } from 'express';
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
port: number;
|
||||||
|
routes: Router;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Server {
|
||||||
|
private app = express();
|
||||||
|
private readonly port: number;
|
||||||
|
private readonly routes: Router;
|
||||||
|
|
||||||
|
constructor (options : Options) {
|
||||||
|
const {port, routes} = options;
|
||||||
|
this.port = port;
|
||||||
|
this.routes = routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
/*middleware*/
|
||||||
|
// para recebir el body como json
|
||||||
|
this.app.use(express.json());
|
||||||
|
// para recebir el body como urlencoded
|
||||||
|
this.app.use(express.urlencoded({ extended: true }));
|
||||||
|
//* Public folder
|
||||||
|
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
this.app.use(this.routes);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ponner servido a escuchar en el puerto 8080
|
||||||
|
this.app.listen(this.port, () => {
|
||||||
|
console.log(`Server is running on port ${this.port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user