Skip to content

Commit

Permalink
Update Customer Register (#4)
Browse files Browse the repository at this point in the history
* ✨ 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
nmdra committed Aug 8, 2024
1 parent d75d3d2 commit bd5e0df
Show file tree
Hide file tree
Showing 39 changed files with 988 additions and 13,368 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/Jest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ jobs:
with:
node-version: '22'
- name: Install Node Dependencies
run: npm ci
run: npm install
- name: Run Jest Testing
run: npm run test -- --ci --json --coverage --testLocationInResults --outputFile=jestReport.json
6 changes: 3 additions & 3 deletions .github/workflows/lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ jobs:
name: Lint frontend
uses: ./.github/workflows/Eslint-Frontend.yml

Lint-Test:
name: Jest Testing
uses: ./.github/workflows/Jest.yml
# Lint-Test:
# name: Jest Testing
# uses: ./.github/workflows/Jest.yml
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.env
node_modules
.vercel
package-lock.json
# Logs
logs
*.log
Expand Down
7 changes: 6 additions & 1 deletion backend/.env.example
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
268 changes: 268 additions & 0 deletions backend/controllers/userController.js
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');
}
}
3 changes: 1 addition & 2 deletions backend/eslint.config.mjs
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,
];
5 changes: 5 additions & 0 deletions backend/middlewares/asyncHandler.js
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;
32 changes: 32 additions & 0 deletions backend/middlewares/authMiddleware.js
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
18 changes: 15 additions & 3 deletions backend/middlewares/errorMiddleware.js
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 }
Loading

0 comments on commit bd5e0df

Please sign in to comment.