-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* ✨ feat: User Login UI * ➖ Remove Jest * ✨ feat: User email Verification * 💚 fix: remove jest action 🔀 Update User login feature (#8) * ✨ feat: password reset * ✨ feat: Add Update Profile * 🩹 Update ErrorMiddleware
- Loading branch information
Showing
39 changed files
with
988 additions
and
13,368 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
.env | ||
node_modules | ||
.vercel | ||
package-lock.json | ||
# Logs | ||
logs | ||
*.log | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
PORT=<PORT_NUMBER> | ||
NODE_ENV=development | ||
MONGODB_URI=<MongoDB_URI> | ||
JWT_SECRET=<JWT_SECRET> | ||
JWT_SECRET=<JWT_SECRET> | ||
|
||
EMAIL_USER=<your email that use for sending email to others> | ||
EMAIL_PASS=<your email app password, it will be 16digits> | ||
EMAIL_SERVICE=gmail | ||
SITE_URL=http://localhost:3000 // this url url for verification email when front-end run on localhost, change this when use on production server |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
import User from '../models/userModel.js' | ||
import { generateToken, tokenToVerify } from '../utils/generateToken.js' | ||
import { sendEmail } from '../utils/sendEmail.js' | ||
import jwt from 'jsonwebtoken' | ||
|
||
// @desc Register a new user | ||
// @route POST /api/users | ||
// @access Public | ||
export const registerUser = async (req, res, next) => { | ||
const { name, email, password, role, defaultAddress, contactNumber, pic } = | ||
req.body | ||
|
||
try { | ||
const isUserExist = await User.findOne({ email }) | ||
if (isUserExist) { | ||
res.status(400) | ||
throw new Error('User already exists') | ||
} | ||
|
||
const user = await User.create({ | ||
name, | ||
email, | ||
password, | ||
role, | ||
defaultAddress, | ||
contactNumber, | ||
pic | ||
}) | ||
|
||
if (user) { | ||
generateToken(res, user._id) | ||
sendVerifyEmail(user) | ||
res.json({ | ||
_id: user._id, | ||
name: user.name, | ||
email: user.email, | ||
role: user.role, | ||
defaultAddress: user.defaultAddress, | ||
contactNumber: user.contactNumber, | ||
}) | ||
} else { | ||
res.status(500) | ||
throw new Error('User creation failed') | ||
} | ||
} catch (error) { | ||
return next(error) | ||
} | ||
} | ||
|
||
// @desc Auth user & get token | ||
// @route POST /api/users/auth | ||
// @access Public | ||
export const authUser = async (req, res, next) => { | ||
const { email, password } = req.body | ||
try { | ||
if (!email || !password) { | ||
res.status(400) | ||
throw new Error('Email and password are required') | ||
} | ||
const user = await User.findOne({ email }) | ||
|
||
if (user && (await user.matchPassword(password))) { | ||
generateToken(res, user._id) | ||
|
||
res.json({ | ||
_id: user._id, | ||
name: user.name, | ||
email: user.email, | ||
role: user.role, | ||
defaultAddress: user.defaultAddress, | ||
contactNumber: user.contactNumber, | ||
}) | ||
} else { | ||
res.status(401).json({ message: "Invalid Password or Email" }) | ||
} | ||
} catch (error) { | ||
return next(error) | ||
} | ||
} | ||
|
||
// @desc Logout user / clear cookie | ||
// @route POST /api/users/logout | ||
// @access Public | ||
export const logoutUser = (_req, res) => { | ||
res.cookie('jwt', '', { | ||
httpOnly: true, | ||
expires: new Date(0), | ||
}) | ||
res.status(200).json({ message: 'Logged out successfully' }) | ||
} | ||
|
||
export const updateUserProfile = async (req, res, next) => { | ||
try { | ||
const user = await User.findById(req.user._id) | ||
|
||
if (user) { | ||
user.name = req.body.name || user.name | ||
user.email = req.body.email || user.email | ||
user.password = req.body.password || user.password | ||
user.defaultAddress = req.body.defaultAddress || user.defaultAddress | ||
user.contactNumber = req.body.contactNumber || user.contact | ||
// user.picture = req.body.picture || user.picture | ||
// user.role = user.role; | ||
// user.password = req.body.password || user.password | ||
|
||
if (req.body.password) { | ||
user.password = req.body.password | ||
} | ||
|
||
const updatedUser = await user.save() | ||
|
||
res.status(201).json({ | ||
message: 'User updated successfully', | ||
_id: updatedUser._id, | ||
name: updatedUser.name, | ||
email: updatedUser.email, | ||
role: updatedUser.role, | ||
contactNumber: updatedUser.contactNumber | ||
}) | ||
} else { | ||
res.status(404) | ||
throw new Error('User not found') | ||
} | ||
} catch (error) { | ||
return next(error) | ||
} | ||
} | ||
|
||
// @desc Get user by ID | ||
// @route GET /api/users/:id | ||
// @access Private/Admin | ||
export const getUserById = async (req, res, next) => { | ||
if (req.user.role !== 'admin') { | ||
return res.status(403).json('Unauthorized') | ||
} | ||
|
||
try { | ||
const user = await User.findById(req.params.id).select('-password') | ||
|
||
if (user) { | ||
res.json(user); | ||
} else { | ||
return res.status(404).json('User not found') | ||
} | ||
} catch (error) { | ||
next(error) | ||
} | ||
} | ||
|
||
// @desc Get user profile | ||
// @route GET /api/users/profile | ||
// @access Private | ||
export const getUserProfile = async (req, res) => { | ||
const user = await User.findById(req.user._id); | ||
|
||
if (user) { | ||
res.json(user); | ||
} else { | ||
res.status(404) | ||
throw new Error('User not found') | ||
} | ||
} | ||
|
||
// @desc Upload User Profile picture | ||
// @route GET /api/users/pfp | ||
// @access Private | ||
|
||
// @desc Send Verify Email | ||
// @route GET /api/users/verify | ||
// @access Private | ||
export const sendVerifyEmail = async (user, res) => { | ||
try { | ||
const token = tokenToVerify(user.email); | ||
const body = { | ||
from: `'FarmCart 🌱' <${process.env.EMAIL_USER}>`, | ||
to: `${user.email}`, | ||
subject: 'FarmCart: Email Activation', | ||
html: ` | ||
<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;"> | ||
<h2 style="color: #22c55e;">Hello ${user.name},</h2> | ||
<p>Thank you for signing up with <strong>FarmCart</strong>. Please verify your email address to complete your registration.</p> | ||
<p>This link will expire in <strong>2 minutes</strong>.</p> | ||
<p style="margin-bottom: 20px;">Click the button below to activate your account:</p> | ||
<a href="${process.env.SITE_URL}/api/users/verify?token=${token}" | ||
style="background: #22c55e; color: white; border: 1px solid #22c55e; padding: 10px 15px; border-radius: 4px; text-decoration: none; display: inline-block;">Verify Account</a> | ||
<p style="margin-top: 35px;">If you did not initiate this request, please contact us immediately at support@farmcart.com</p> | ||
<p style="margin-bottom: 0;">Thank you,</p> | ||
<p style="font-weight: bold;">The FarmCart Team</p> | ||
</div> | ||
`, | ||
}; | ||
|
||
const message = 'Please check your email to verify!'; | ||
await sendEmail(body, message); | ||
return res.status(200).json({ success: true, message }); | ||
} catch (error) { | ||
console.error(`Error in sending verification email: ${error.message}`); | ||
res.status(500) | ||
throw new Error(`Error in sending verification email`) | ||
} | ||
} | ||
|
||
export const verifyEmail = async (req, res) => { | ||
const token = req.query.token | ||
|
||
if (!token) { | ||
return res.status(400).json({ success: false, message: 'Invalid token' }) | ||
} | ||
|
||
try { | ||
const decoded = jwt.verify(token, process.env.JWT_SECRET) | ||
const user = await User.findOne({ email: decoded.email }) | ||
|
||
if (!user) { | ||
return res.status(400).json({ success: false, message: 'Invalid token or user does not exist' }) | ||
} | ||
|
||
user.isVerified = true | ||
await user.save() | ||
|
||
return res.status(200).json({ success: true, message: 'Email verified successfully!' }) | ||
} catch (error) { | ||
return res.status(400).json({ success: false, message: error.message }) | ||
} | ||
} | ||
|
||
// @desc Send Password Reset Email | ||
// @route GET /api/users/forgot-password | ||
// @access Private | ||
|
||
export const forgotPassword = async (req, res) => { | ||
try { | ||
const isAdded = await User.findOne({ email: req.body.verifyEmail }) | ||
if (!isAdded) { | ||
return res.status(404).json({ success: false, message: 'No user found with this email' }) | ||
} | ||
|
||
const token = await tokenToVerify(isAdded.email) | ||
|
||
const body = { | ||
from: `'FarmCart 🌱' <${process.env.EMAIL_USER}>`, | ||
to: `${req.body.verifyEmail}`, | ||
subject: 'FarmCart: Password Reset', | ||
html: `<h2>Hello ${req.body.verifyEmail}</h2> | ||
<p>A request has been received to change the password for your <strong>FarmCart</strong> account </p> | ||
<p>This link will expire in <strong> 15 minutes</strong>.</p> | ||
<p style="margin-bottom:20px;">Click this link for reset your password</p> | ||
<a href="${process.env.SITE_URL}/api/users/reset-pass?token=${token}" | ||
style="background: #ff0000; color: white; border: 1px solid #ff0000; padding: 10px 15px; border-radius: 4px; text-decoration: none; display: inline-block;">Reset Password</a> | ||
<p style="margin-top: 35px;">If you did not initiate this request, please contact us immediately at support@farmcart.com</p> | ||
<p style="margin-bottom:0px;">Thank you</p> | ||
<strong>FarmCart Team</strong> | ||
`, | ||
}; | ||
|
||
const message = 'Please check your email to reset your password!'; | ||
await sendEmail(body); | ||
|
||
return res.status(200).json({ success: true, message }); | ||
} catch (error) { | ||
console.error(`Error in forgotPassword: ${error.message}`); | ||
res.status(500) | ||
throw new Error('Internal server error'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,7 @@ | ||
import globals from "globals"; | ||
import pluginJs from "@eslint/js"; | ||
import jest from 'eslint-plugin-jest' | ||
|
||
export default [ | ||
{languageOptions: { globals: {...globals.browser, ...globals.node} }}, | ||
pluginJs.configs.recommended,jest.configs['flat/recommended'], | ||
pluginJs.configs.recommended, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const asyncHandler = fn => (req, res, next) => { | ||
Promise.resolve(fn(req, res, next)).catch(next); | ||
} | ||
|
||
export default asyncHandler; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import jwt from 'jsonwebtoken' | ||
import User from '../models/userModel.js' | ||
|
||
const protect = async (req, res, next) => { | ||
let token = req.cookies.jwt | ||
|
||
try { | ||
if (token) { | ||
const decoded = await jwt.verify(token, process.env.JWT_SECRET) | ||
|
||
req.user = await User.findById(decoded.userId).select('-password') | ||
|
||
if (!req.user) { | ||
res.status(403) | ||
throw new Error('User not found') | ||
} | ||
|
||
next() | ||
} else { | ||
res.status(401) | ||
throw new Error('Not authorized. No token provided', 401) | ||
} | ||
} catch (error) { | ||
if (error instanceof jwt.JsonWebTokenError) { | ||
res.status(401) | ||
return next(new Error('Invalid token signature')) | ||
} | ||
next(error) | ||
} | ||
} | ||
|
||
export default protect |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,25 @@ | ||
const notFound = (req, res, next) => { | ||
const error = new Error(`Not Found -${req.originalUrl}`); | ||
res.status(404); | ||
next(error); | ||
}; | ||
|
||
const errorHandler = (err, _req, res, next) => { | ||
let statusCode = res.statusCode === 200 ? 500 : res.statusCode | ||
let message = err.message | ||
|
||
//check for Mongoose bad Object | ||
if(err.name === 'CastError' && err.kind === 'ObjectId') { | ||
message = `Resource not founded`; | ||
statusCode = 404; | ||
} | ||
|
||
res.status(statusCode).json({ | ||
message: message, | ||
stack: process.env.NODE_ENV === 'production' ? null : err.stack, | ||
message, | ||
stack: process.env.NODE_ENV === 'production' ? 'null' : err.stack, | ||
}) | ||
|
||
next() | ||
} | ||
|
||
export { errorHandler } | ||
export { errorHandler, notFound } |
Oops, something went wrong.