Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 82 additions & 33 deletions RentalCar/server/controllers/userController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import Car from "../models/Car.js";

const MAX_LOGIN_ATTEMPTS = 5;
const LOCK_TIME_MS = 15 * 60 * 1000; // 15 minuta

//Generate JWT Token
const generateToken = (userId) => {
const payload = userId;
return jwt.sign(payload, process.env.JWT_SECRET);
const generateToken = (user) => {
return jwt.sign(
{
id: user._id.toString(),
role: user.role,
},
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
};

//Register User
Expand All @@ -15,15 +24,15 @@ export const registerUser = async (req, res) => {
const { name, email, password } = req.body;

if (!name || !email || !password || password.length < 8) {
return res.json({
return res.status(400).json({
success: false,
message: "Fill all the fields",
message: "Name, email and password are required, and password must be at least 8 characters long.",
});
}

const userExists = await User.findOne({ email });
if (userExists) {
return res.json({
return res.status(409).json({
success: false,
message: "User already exists",
});
Expand All @@ -37,16 +46,19 @@ export const registerUser = async (req, res) => {
password: hashedPassword,
});

const token = generateToken(user._id.toString());
const token = generateToken(user);

res.json({ success: true, token });
return res.status(201).json({
success: true,
token,
});
} catch (error) {
// error handling
console.log(error.message);
res.json({
success: false,
message: error.message,
});
return res.status(500).json({
success: false,
message: "Server error",
});
}
};

Expand All @@ -56,33 +68,64 @@ export const loginUser = async (req, res) => {
const { email, password } = req.body;

const user = await User.findOne({ email });

const invalidMessage = {
success: false,
message: "Invalid email or password",
};

if (!user) {
return res.json({
return res.status(401).json(invalidMessage);
}

if (user.lockUntil && user.lockUntil > new Date()) {
return res.status(423).json({
success: false,
message: "User not found",
message: "Account is temporarily locked. Please try again later.",
});
}

if (user.lockUntil && user.lockUntil <= new Date()) {
user.loginAttempts = 0;
user.lockUntil = null;
await user.save();
}

const isMatch = await bcrypt.compare(password, user.password);

if (!isMatch) {
return res.json({
success: false,
message: "Invalid Credentials",
});
user.loginAttempts += 1;

if (user.loginAttempts >= MAX_LOGIN_ATTEMPTS) {
user.lockUntil = new Date(Date.now() + LOCK_TIME_MS);
await user.save();

return res.status(423).json({
success: false,
message: "Account is temporarily locked. Please try again later.",
});
}

await user.save();

return res.status(401).json(invalidMessage);
}

const token = generateToken(user._id.toString());
user.loginAttempts = 0;
user.lockUntil = null;
await user.save();

res.json({
const token = generateToken(user);

return res.status(200).json({
success: true,
token,
});
} catch (error) {
// error handling
console.log(error.message);
res.json({
return res.status(500).json({
success: false,
message: error.message,
message: "Server error",
});
}
};
Expand All @@ -92,26 +135,32 @@ export const getUserData = async (req, res) => {
try {
const { user } = req;

res.json({
success: true,
user,
});
return res.status(200).json({
success: true,
user,
});
} catch (error) {
console.log(error.message);
res.json({
success: false,
message: error.message,
});
return res.status(500).json({
success: false,
message: "Server error",
});
}
};

// Get All Cars for the Frontend
export const getCars = async (req, res) => {
try {
const cars = await Car.find({ isAvailable: true });
res.json({ success: true, cars });
return res.status(200).json({
success: true,
cars,
});
} catch (error) {
console.log(error.message);
res.json({ success: false, message: error.message });
return res.status(500).json({
success: false,
message: "Server error",
});
}
};
12 changes: 12 additions & 0 deletions RentalCar/server/middleware/loginRateLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import rateLimit from "express-rate-limit";

export const loginRateLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minuta
max: 5, // max 5 pokusaja u okviru 10 minuta po IP adresi
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
message: "Too many login attempts. Please try again later.",
},
});
7 changes: 5 additions & 2 deletions RentalCar/server/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ const userSchema = new mongoose.Schema(
password: { type: String, required: true },
role: {
type: String,
enum: ["user","owner","admin"],
enum: ["user", "owner", "admin"],
default: "user",
},
image: { type: String, default: "" },

documents: [{ type: mongoose.Schema.Types.ObjectId, ref: "Document" }],

loginAttempts: { type: Number, default: 0 },
lockUntil: { type: Date, default: null },
},
{ timestamps: true }
);

const User = mongoose.model("User", userSchema);

export default User;
export default User;
3 changes: 2 additions & 1 deletion RentalCar/server/routes/userRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
registerUser,
} from "../controllers/userController.js";
import { protect } from "../middleware/auth.js";
import { loginRateLimiter } from "../middleware/loginRateLimiter.js";

const userRouter = express.Router();

Expand Down Expand Up @@ -58,7 +59,7 @@ userRouter.post("/register", registerUser);
* schema:
* $ref: '#/components/schemas/AuthResponse'
*/
userRouter.post("/login", loginUser);
userRouter.post("/login", loginRateLimiter, loginUser);

/**
* @openapi
Expand Down