diff --git a/RentalCar/server/controllers/userController.js b/RentalCar/server/controllers/userController.js index 295878c..d9a9d01 100644 --- a/RentalCar/server/controllers/userController.js +++ b/RentalCar/server/controllers/userController.js @@ -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 @@ -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", }); @@ -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", + }); } }; @@ -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", }); } }; @@ -92,16 +135,16 @@ 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", + }); } }; @@ -109,9 +152,15 @@ export const getUserData = async (req, res) => { 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", + }); } }; diff --git a/RentalCar/server/middleware/loginRateLimiter.js b/RentalCar/server/middleware/loginRateLimiter.js new file mode 100644 index 0000000..3984569 --- /dev/null +++ b/RentalCar/server/middleware/loginRateLimiter.js @@ -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.", + }, +}); \ No newline at end of file diff --git a/RentalCar/server/models/User.js b/RentalCar/server/models/User.js index b5011ad..2114f9d 100644 --- a/RentalCar/server/models/User.js +++ b/RentalCar/server/models/User.js @@ -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; \ No newline at end of file diff --git a/RentalCar/server/routes/userRoutes.js b/RentalCar/server/routes/userRoutes.js index 965a8fa..a79cefb 100644 --- a/RentalCar/server/routes/userRoutes.js +++ b/RentalCar/server/routes/userRoutes.js @@ -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(); @@ -58,7 +59,7 @@ userRouter.post("/register", registerUser); * schema: * $ref: '#/components/schemas/AuthResponse' */ -userRouter.post("/login", loginUser); +userRouter.post("/login", loginRateLimiter, loginUser); /** * @openapi