Skip to content

Commit

Permalink
backend: created user model and authMiddleware
Browse files Browse the repository at this point in the history
This commit adds:
- User Model: Created with name and password fields.
- Auth Middleware: JWT authentication middleware.
- User controller: Login and Register user.
- Auth Endpoints: Register and login endpoints.

Fixes: #62
  • Loading branch information
sethdivyansh committed Jun 8, 2024
1 parent 19abe9b commit 584ec25
Show file tree
Hide file tree
Showing 11 changed files with 673 additions and 26 deletions.
Binary file added .DS_Store
Binary file not shown.
Binary file added backend/.DS_Store
Binary file not shown.
492 changes: 468 additions & 24 deletions backend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
"dependencies": {
"@types/node": "^20.12.13",
"@types/uuid": "^9.0.8",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.4.0",
"ts-node": "^10.9.2",
"uuid": "^9.0.1"
Expand All @@ -27,6 +29,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"eslint": "^8.57.0",
"eslint-config-standard-with-typescript": "^43.0.1",
Expand Down
80 changes: 80 additions & 0 deletions backend/src/controllers/userController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Request, Response } from 'express';
import { catchError, ControllerFunction } from '../utils';
import jwt from 'jsonwebtoken';
import { IUser, User } from '../models/userModel';
import { Types } from 'mongoose';
import bcrypt from 'bcrypt';

const getUserToken = (_id: string | Types.ObjectId): string => {
const token = jwt.sign({ _id }, process.env.JWT_SECRET as string, {
expiresIn: '30d',
});
return token;
};

const registerUser: ControllerFunction = catchError(
async (req: Request, res: Response): Promise<void> => {
const { username, password }: IUser = req.body;

if ([username, password].some((field) => field?.trim() === '')) {
throw new Error('All fields are required');
}
const existingUser = await User.findOne({ username });
if (existingUser) {
throw new Error('User already exists');
}

const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
username,
password: hashedPassword,
});

const createdUser = await User.findById(user._id).select('-password');

if (!createdUser) {
throw new Error('Failed to create user');
}

res.status(201).send({
message: 'User created successfully',
createdUser,
});
}
);

const loginUser: ControllerFunction = catchError(
async (req: Request, res: Response): Promise<void> => {
const { username, password }: IUser = req.body;

if (!username) {
throw new Error('Username is required');
}

const user = await User.findOne({ username });
if (!user) {
throw new Error('User not found');
}

const isPasswordCorrect = await user.isPasswordCorrect(password);
if (!isPasswordCorrect) {
throw new Error('Invalid credentials');
}

const token: string = getUserToken((user as IUser)._id as string);

const loggedInUser = await User.findById(user._id).select('-password');

const options = {
httpOnly: true,
secure: true,
};

res.status(200).cookie('accessToken', token, options).json({
token,
user: loggedInUser,
});
}
);

export { registerUser, loginUser };
5 changes: 5 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ app.get('/', (req, res) => {
res.send('Hello from the backend!');
});

//Routes
import userRoutes from './routes/userRoutes.js';

app.use('/api/v1/auth', userRoutes);

app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
39 changes: 39 additions & 0 deletions backend/src/middlewares/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import jwt from 'jsonwebtoken';
import User, { IUser } from '../models/userModel';
import { NextFunction, Request, Response } from 'express';

export interface AuthRequest extends Request {
user: IUser;
}

export const verifyToken = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
try {
const accessToken: string = req.cookies.accessToken;

if (!accessToken) {
return res.status(401).json({ error: 'Access token is required' });
}

const { _id } = jwt.verify(
accessToken,
process.env.JWT_SECRET as string
) as { _id: string };

const user: IUser | null = await User.findOne({ _id });

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

req.user = user;
next();
} catch (error) {
return res.status(401).json({
error: (error as Error)?.message || 'Invalid access token',
});
}
};
44 changes: 44 additions & 0 deletions backend/src/models/userModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import mongoose from 'mongoose';
import bcrypt from 'bcrypt';
import { NextFunction } from 'express';

export interface IUser extends mongoose.Document {
username: string;
password: string;
isPasswordCorrect: (password: string) => Promise<boolean>;
}

const userSchema = new mongoose.Schema<IUser>(
{
username: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters long'],
},
},
{
timestamps: true,
}
);

userSchema.pre('save', async function (next: NextFunction): Promise<void> {
if (!this.isModified('password')) return next();

this.password = await bcrypt.hash(this.password, 10);
next();
});

userSchema.methods.isPasswordCorrect = async function (
password: string
): Promise<boolean> {
return await bcrypt.compare(password, this.password);
};

const User = mongoose.model<IUser>('User', userSchema);

export { User };
9 changes: 9 additions & 0 deletions backend/src/routes/userRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from 'express';
import { loginUser, registerUser } from '../controllers/userController';

const router = express.Router();

router.route('/register').post(registerUser);
router.route('/login').post(loginUser);

export default router;
4 changes: 2 additions & 2 deletions backend/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Request, Response } from 'express';

type ControllerFunction = (req: Request, res: Response) => Promise<void>;
export type ControllerFunction = (req: Request, res: Response) => Promise<void>;

export function catchError(fn: ControllerFunction): ControllerFunction {
return async function (req: Request, res: Response) {
try {
return await fn(req, res);
} catch (error) {
res.status(500).json({ error: error });
res.status(500).json({ error: (error as Error).message });
}
};
}
23 changes: 23 additions & 0 deletions backend/tests/userModel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import dotenv from 'dotenv';
import mongoose from 'mongoose';

dotenv.config();

describe('insert user', () => {
beforeAll(async () => {
await mongoose.connect(`${process.env.MONGO_URI}/users`);
});

afterAll(async () => {
await mongoose.connection.close();
});

it('should insert a user into collection', async () => {
const users = mongoose.connection.collection('users');
const mockUser = { name: 'user1234', password: 'pass1' };
await users.insertOne(mockUser);

const insertedUser = await users.findOne({ name: 'user1234' });
expect(insertedUser).toEqual(mockUser);
});
});

0 comments on commit 584ec25

Please sign in to comment.