From 5cd59ac66b6652503f67bde15a941d2d9cd4a157 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 11:41:31 +0200 Subject: [PATCH 01/13] feat: add `signup` function --- src/controllers/auth.controller.js | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/controllers/auth.controller.js diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 0000000..614c9da --- /dev/null +++ b/src/controllers/auth.controller.js @@ -0,0 +1,68 @@ +import * as dotenv from 'dotenv'; +dotenv.config(); + +import prisma from '../config/prismaClient.js'; +import { hashPassword } from '../utils/password.utils.js'; +import { generateOTP } from '../utils/otp.utils.js'; +import { sendEmail } from '../utils/email.utils.js'; + +/* eslint no-undef:off */ +// Authentication Controllers +export const signup = async (req, res) => { + try { + // TODO: Validate signup function + + const { email, password, name, username } = req.body; + + // Check if user already exists + const existingUser = await prisma.user.findUnique({ + where: { + email, + username, + }, + }); + + if (existingUser) { + return res.status(400).json({ + message: 'User with this email or username already exists', + }); + } + + // Hash password + const hashedPassword = await hashPassword(password); + + // Generate email verification OTP + const verificationOTP = generateOTP(); + // TODO: hash otp when you save it in the db + const otpExpiry = new Date(Date.now() + 10 * 60 * 1000); + + // Create user + const user = await prisma.user.create({ + data: { + email, + username, + name, + password: hashedPassword, + role: 'MEMBER', // Default role + isActive: false, // Require email verification + emailVerificationToken: verificationOTP, + emailVerificationExpires: otpExpiry, + }, + }); + + // Send verification email + await sendEmail( + email, + 'Verify Your Email', + `Your verification code is: ${verificationOTP}`, + ); + + return res.status(201).json({ + message: 'User created. Please verify your email.', + userId: user.id, + user: user, + }); + } catch (error) { + return res.status(500).json({ message: `Signup failed: ${error.message}` }); + } +}; From 0eae3db59676aa00d1946864d04d0827b4990649 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 11:46:00 +0200 Subject: [PATCH 02/13] feat: add `signup` router --- src/routes/auth.routes.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/routes/auth.routes.js diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 0000000..6a07ce1 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { signup } from '../controllers/auth.controller'; + +const router = Router(); + +router.post('/api/auth/signup', signup); + +export default router; From ac7f9b9457ff62e2432e9064aed7e91241361399 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 12:41:03 +0200 Subject: [PATCH 03/13] fix: add parse application/json middlewares --- src/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.js b/src/index.js index 20cdf48..c942341 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,17 @@ import express from 'express'; import * as dotenv from 'dotenv'; dotenv.config(); +import authRouter from './routes/auth.routes.js'; /* eslint no-undef: off */ const PORT = process.env.PORT; const app = express(); +app.use(express.json()); // for parsing application/json +app.use(express.urlencoded({ extended: true })); + +app.use(authRouter); + app.listen(PORT, () => { /* eslint no-console:off */ console.log( From 435466d40e8d370d506d37db2a4e571bf7600728 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 12:41:53 +0200 Subject: [PATCH 04/13] refactor: import `signup` function --- src/routes/auth.routes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index 6a07ce1..1c06722 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { signup } from '../controllers/auth.controller'; +import { signup } from '../controllers/auth.controller.js'; const router = Router(); From dc414884338f8d4bc42f6f964960f08d788c347c Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 12:42:52 +0200 Subject: [PATCH 05/13] fix: parse email options parameters --- src/controllers/auth.controller.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 614c9da..5e42ea8 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -15,7 +15,7 @@ export const signup = async (req, res) => { const { email, password, name, username } = req.body; // Check if user already exists - const existingUser = await prisma.user.findUnique({ + const existingUser = await prisma.user.findFirst({ where: { email, username, @@ -51,11 +51,11 @@ export const signup = async (req, res) => { }); // Send verification email - await sendEmail( - email, - 'Verify Your Email', - `Your verification code is: ${verificationOTP}`, - ); + await sendEmail({ + to: email, + subject: 'Verify Your Email', + text: `Your verification code is: ${verificationOTP}`, + }); return res.status(201).json({ message: 'User created. Please verify your email.', From bfdec1b5dc3b0bc9cda6174d40317a28c52d5f7a Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 12:45:02 +0200 Subject: [PATCH 06/13] refactor: email transporter --- src/utils/email.utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/email.utils.js b/src/utils/email.utils.js index a651672..ed630e6 100644 --- a/src/utils/email.utils.js +++ b/src/utils/email.utils.js @@ -6,6 +6,10 @@ const transporter = nodemailer.createTransport({ user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, + pool: true, + maxConnections: 1, + rateDelta: 20000, // 20 seconds + rateLimit: 5, // max 5 emails per rateDelta }); /* eslint no-undef: off */ From 5f515893cd8365e5962315b471128472248699a6 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 12:52:56 +0200 Subject: [PATCH 07/13] refactor: email and token utils --- src/utils/email.utils.js | 2 +- src/utils/token.utils.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/email.utils.js b/src/utils/email.utils.js index ed630e6..b626ec7 100644 --- a/src/utils/email.utils.js +++ b/src/utils/email.utils.js @@ -15,7 +15,7 @@ const transporter = nodemailer.createTransport({ /* eslint no-undef: off */ export const sendOTPEmail = async (email, otp) => { await transporter.sendMail({ - from: process.env.EMAIL_FROM, + from: process.env.EMAIL_USER, to: email, subject: 'Verify your Email', text: `Your OTP for email verification is: ${otp}. It will expire in 10 minutes.`, diff --git a/src/utils/token.utils.js b/src/utils/token.utils.js index 48e4203..9fdb35f 100644 --- a/src/utils/token.utils.js +++ b/src/utils/token.utils.js @@ -8,8 +8,8 @@ export const generateAccessToken = (user) => { email: user.email, role: user.role, }, - process.env.JWT_SECRET, - { expiresIn: '7d' }, + process.env.JWT_ACCESS_SECRET, + { expiresIn: '1h' }, ); }; @@ -18,7 +18,7 @@ export const generateRefreshToken = (user) => { { id: user.id, }, - process.env.JWT_SECRET, - { expiresIn: '30d' }, + process.env.JWT_REFRESH_SECRET, + { expiresIn: '7d' }, ); }; From 9759a49014e6d55d02389950bd2493ea407fdfcc Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 13:05:00 +0200 Subject: [PATCH 08/13] refactor: update user model --- .../migrations/20250327110352_fix_auth/migration.sql | 12 ++++++++++++ prisma/schema/models/user.model.prisma | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250327110352_fix_auth/migration.sql diff --git a/prisma/migrations/20250327110352_fix_auth/migration.sql b/prisma/migrations/20250327110352_fix_auth/migration.sql new file mode 100644 index 0000000..32f7b62 --- /dev/null +++ b/prisma/migrations/20250327110352_fix_auth/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `name` on the `users` table. All the data in the column will be lost. + - Added the required column `firstName` to the `users` table without a default value. This is not possible if the table is not empty. + - Added the required column `lastName` to the `users` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "users" DROP COLUMN "name", +ADD COLUMN "firstName" VARCHAR(100) NOT NULL, +ADD COLUMN "lastName" VARCHAR(100) NOT NULL; diff --git a/prisma/schema/models/user.model.prisma b/prisma/schema/models/user.model.prisma index 1ed9c1a..35dc9f4 100644 --- a/prisma/schema/models/user.model.prisma +++ b/prisma/schema/models/user.model.prisma @@ -3,7 +3,8 @@ model User { email String @unique @db.VarChar(255) username String @unique @db.VarChar(50) password String @db.VarChar(255) - name String @db.VarChar(100) + firstName String @db.VarChar(100) + lastName String @db.VarChar(100) role UserRole profilePic String? departmentId String? @db.Uuid From 3bfe0d791f67877e220ecc9c2222ad62f2b882ec Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 13:06:37 +0200 Subject: [PATCH 09/13] fix: user name --- src/controllers/auth.controller.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 5e42ea8..cf75060 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -12,7 +12,7 @@ export const signup = async (req, res) => { try { // TODO: Validate signup function - const { email, password, name, username } = req.body; + const { email, password, firstName, lastName, username } = req.body; // Check if user already exists const existingUser = await prisma.user.findFirst({ @@ -41,7 +41,8 @@ export const signup = async (req, res) => { data: { email, username, - name, + firstName, + lastName, password: hashedPassword, role: 'MEMBER', // Default role isActive: false, // Require email verification From 30599dca3c4758f15bb567d01c568fa163ad0fd4 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 13:09:57 +0200 Subject: [PATCH 10/13] feat: add validation for `signup` controller --- src/validations/auth.validations.js | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/validations/auth.validations.js diff --git a/src/validations/auth.validations.js b/src/validations/auth.validations.js new file mode 100644 index 0000000..ef0e573 --- /dev/null +++ b/src/validations/auth.validations.js @@ -0,0 +1,33 @@ +import Joi from 'joi'; + +export const signupValidation = (obj) => { + const schema = Joi.object({ + firstName: Joi.string().required().trim().min(3).max(30).messages({ + 'string.empty': 'First name is required', + 'string.min': 'First name must be at least 3 characters long.', + 'string.max': 'First name must be at most 30 characters long.', + }), + lastName: Joi.string().required().trim().messages({ + 'string.empty': 'Last name is required', + 'string.min': 'Last name must be at least 3 characters long.', + 'string.max': 'Last name must be at most 30 characters long.', + }), + username: Joi.alphanum().trim().required().messages({ + 'string.alphanum': 'Username must contain only alphanumeric characters', + 'string.min': 'Username must be at least 3 characters long', + 'string.max': 'Username cannot be longer than 30 characters', + 'any.required': 'Username is required', + }), + email: Joi.string().email().required().trim().messages({ + 'string.empty': 'Email is required.', + 'string.email': 'Please enter a valid email address.', + }), + password: Joi.string().required().trim().min(8).max(32).messages({ + 'string.empty': 'Password is required.', + 'string.min': 'Password must be at least 8 characters long.', + 'string.max': 'Password must be at most 32 characters long.', + }), + }); + + return schema.validate(obj); +}; From 6b8cc0beecb9ce336fb1fc078091e0498beac0a5 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 13:10:34 +0200 Subject: [PATCH 11/13] chore: setup `joi` --- package-lock.json | 50 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 51 insertions(+) diff --git a/package-lock.json b/package-lock.json index 46473e5..a0b5e0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "crypto": "^1.0.1", "dotenv": "^16.4.7", "express": "^4.21.2", + "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", "nodemailer": "^6.10.0" }, @@ -1051,6 +1052,21 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1200,6 +1216,27 @@ "@prisma/debug": "6.5.0" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, "node_modules/@supabase/auth-js": { "version": "2.68.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz", @@ -3117,6 +3154,19 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 7aeccc8..74212b2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "crypto": "^1.0.1", "dotenv": "^16.4.7", "express": "^4.21.2", + "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", "nodemailer": "^6.10.0" }, From 8cb376242a98a78bf57e5ed96d01a2d8c17f2793 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 13:15:48 +0200 Subject: [PATCH 12/13] fix: alphanum in joi validation --- src/validations/auth.validations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validations/auth.validations.js b/src/validations/auth.validations.js index ef0e573..d67f9e0 100644 --- a/src/validations/auth.validations.js +++ b/src/validations/auth.validations.js @@ -12,7 +12,7 @@ export const signupValidation = (obj) => { 'string.min': 'Last name must be at least 3 characters long.', 'string.max': 'Last name must be at most 30 characters long.', }), - username: Joi.alphanum().trim().required().messages({ + username: Joi.string().trim().required().messages({ 'string.alphanum': 'Username must contain only alphanumeric characters', 'string.min': 'Username must be at least 3 characters long', 'string.max': 'Username cannot be longer than 30 characters', From da89c0a3a93a76adbabfc1313c9373cffd3797a9 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 27 Mar 2025 13:16:19 +0200 Subject: [PATCH 13/13] feat: add validation in signup controller --- src/controllers/auth.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index cf75060..c75998d 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -5,12 +5,16 @@ import prisma from '../config/prismaClient.js'; import { hashPassword } from '../utils/password.utils.js'; import { generateOTP } from '../utils/otp.utils.js'; import { sendEmail } from '../utils/email.utils.js'; +import { signupValidation } from '../validations/auth.validations.js'; /* eslint no-undef:off */ // Authentication Controllers export const signup = async (req, res) => { try { - // TODO: Validate signup function + const { error } = signupValidation(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } const { email, password, firstName, lastName, username } = req.body;