Skip to content
Merged
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
12 changes: 12 additions & 0 deletions prisma/migrations/20250327110352_fix_auth/migration.sql
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion prisma/schema/models/user.model.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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';
import { signupValidation } from '../validations/auth.validations.js';

/* eslint no-undef:off */
// Authentication Controllers
export const signup = async (req, res) => {
try {
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;

// Check if user already exists
const existingUser = await prisma.user.findFirst({
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,
firstName,
lastName,
password: hashedPassword,
role: 'MEMBER', // Default role
isActive: false, // Require email verification
emailVerificationToken: verificationOTP,
emailVerificationExpires: otpExpiry,
},
});

// Send verification email
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.',
userId: user.id,
user: user,
});
} catch (error) {
return res.status(500).json({ message: `Signup failed: ${error.message}` });
}
};
6 changes: 6 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
8 changes: 8 additions & 0 deletions src/routes/auth.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Router } from 'express';
import { signup } from '../controllers/auth.controller.js';

const router = Router();

router.post('/api/auth/signup', signup);

export default router;
6 changes: 5 additions & 1 deletion src/utils/email.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ 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 */
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.`,
Expand Down
8 changes: 4 additions & 4 deletions src/utils/token.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
);
};

Expand All @@ -18,7 +18,7 @@ export const generateRefreshToken = (user) => {
{
id: user.id,
},
process.env.JWT_SECRET,
{ expiresIn: '30d' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' },
);
};
33 changes: 33 additions & 0 deletions src/validations/auth.validations.js
Original file line number Diff line number Diff line change
@@ -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.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',
'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);
};