diff --git a/app.js b/app.js index 86ad9ce3..6b7a0e4a 100644 --- a/app.js +++ b/app.js @@ -6,6 +6,7 @@ const errorHandlerMiddleware = require('./middlewares/error-handler'); const userRoutes = require('./routes/users'); const orgRoutes = require('./routes/orgRoutes'); const lunchRoutes = require('./routes/lunchRoutes'); +const authRoutes = require('./routes/auth.route'); const sequelize = require('./db/db'); const app = express(); @@ -15,13 +16,14 @@ app.use(express.json()); app.use(helmet()); const PORT = process.env.PORT || 4000; -app.use('/api/organization', orgRoutes); app.use('/api/', userRoutes); +app.use('/api/auth', authRoutes); +app.use('/api/organization', orgRoutes); app.use('/api/lunch', lunchRoutes); // Middlewares -app.use(notFound); app.use(errorHandlerMiddleware); +app.use(notFound); sequelize.sync().then(() => { // Remove console.log() before production diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 00000000..00c91e7c --- /dev/null +++ b/controllers/authController.js @@ -0,0 +1,143 @@ +/* eslint-disable camelcase */ +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcrypt'); +const User = require('../models/user.model'); +const { createCustomError } = require('../errors/custom-errors'); +const Invite = require('../models/organisation_invite.model'); + +const secretKey = process.env.JWT_SECRET_KEY; + +async function createUser(req, res, next) { + try { + const { + first_name, + last_name, + email, + phone, + password, + is_admin, + profile_pic, + org_id, + lunch_credit_balance, + refresh_token, + bank_code, + bank_name, + bank_number, + token, + } = req.body; + + // Validate input data + + if (!first_name || !last_name || !email || !password || !token) { + // TODO: truly validate data + throw createCustomError('Missing required fields', 400); + } + + // Check if the token is valid and retrieve org_id + const invite = await Invite.findOne({ where: { token } }); + + if (!invite || new Date() > invite.ttl) { + throw createCustomError('Invalid or expired invitation token', 400); + } + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const user = { + first_name, + last_name, + email, + phone, + password_hash: hashedPassword, + is_admin, + profile_pic, + org_id, + lunch_credit_balance, + refresh_token, + bank_code, + bank_name, + bank_number, + }; + + const newUser = await User.create(user); + delete newUser.password_hash; + + const userWithoutPassword = Object.assign(newUser.toJSON); + delete userWithoutPassword.password_hash; + console.log(userWithoutPassword); + + return res.status(200).json({ + success: true, + message: 'User registered successfully', + data: { + user: userWithoutPassword, + }, + }); + } catch (error) { + if (error.name === 'SequelizeUniqueConstraintError') { + // Unique constraint violation (duplicate email) + let errorMessage = error.errors[0].message; + errorMessage = errorMessage[0].toUpperCase() + errorMessage.slice(1); + next(createCustomError(errorMessage, 400)); + } + next(error.message); + } +} + +const loginUser = async (req, res, next) => { + const { email, password } = req.body; + + try { + if (!email || !password) { + throw createCustomError('Fill all required fields', 400); + } + + console.log(1); + const user = await User.findOne({ where: { email } }); + if (!user) { + throw createCustomError('Invalid credentials', 404); + } + + const isPasswordValid = await bcrypt.compare(password, user.password_hash); + + if (!isPasswordValid) { + throw createCustomError('Invalid credentials', 401); + } + + const token = jwt.sign({ id: user.id }, secretKey, { + expiresIn: '1h', + }); + + // Sending the token in the response + + return res.status(200).json({ + message: 'User authenticated successfully', + statusCode: 200, + data: { + access_token: token, + email: user.email, + id: user.id, + isAdmin: user.is_admin, + }, + }); + } catch (error) { + next(error); + } +}; + +const logoutUser = (req, res) => { + try { + const token = req.header('Authorization').replace('Bearer ', ''); + + jwt.verify(token, process.env.JWT_SECRET_KEY); + if (!token) { + createCustomError('User is not logged in.', 401); + } + + return res.status(200).json({ message: 'User logged out successfully.' }); + } catch (error) { + return res.status(200).json({ message: 'User logged out successfully.' }); + } +}; + +module.exports = { createUser, loginUser, logoutUser }; diff --git a/controllers/index.js b/controllers/index.js deleted file mode 100644 index e69de29b..00000000 diff --git a/controllers/mailController.js b/controllers/mailController.js new file mode 100644 index 00000000..c4473716 --- /dev/null +++ b/controllers/mailController.js @@ -0,0 +1,94 @@ +// import bcrypt from "bcryptjs" +const dotenv = require('dotenv'); +// const { transport } = require('../config/nodemailerConfig.js'); + +const { transport } = {}; + +dotenv.config(); +// const teamMail = process.env.TEAM_MAIL; + +const issueOtp = async () => { + const otp = `${Math.floor(1000 + Math.random() * 9000)}`; + // const saltRounds = 12; //This should be in environment variable + + // const hashedOTP = await bcrypt.hash(otp, saltRounds); + + //Save hased otp with userId and email for confirmation purposes + //Hased OTP should be saved to db for confirmation later,then deleted upon successful authentication + + return { + userOtp: otp, + timeLeft: `1 hour`, + }; +}; + +const otpMessage = (otp, timeLeft) => { + const template = ` +
+

Welcome to XXXX 2.0

+

+

Your OTP is ${otp}

+

It expires in ${timeLeft}

+
+
+ `; + return template; +}; + +// Function to send email with otp code +const sendEmail = async (email, message) => { + const mailOptions = { + from: 'team.lightning.hng@gmail.com', //This should be in environement variable + subject: 'Verify your email', + to: email, + html: message, + }; + + return new Promise((resolve, reject) => { + transport.sendMail(mailOptions, (err, info) => { + if (err) reject(err); + resolve(info); + }); + }); +}; + +const sendUserOtp = async (userId, email) => { + try { + if (!userId || !email) { + return { + status: false, + message: `User details cannot be empty`, + }; + } + + //generate a new otp + const otp = await issueOtp(userId, email); + const message = otpMessage(otp.userOtp, otp.timeLeft); + + //send mail with otp details + await sendEmail(email, message); + + return { + status: true, + message: 'otp sent successfully', + data: null, + }; + } catch (error) { + console.log(error); + return { + status: false, + message: `internal server error`, + }; + } +}; + +module.exports = { + issueOtp, + otpMessage, + sendEmail, + sendUserOtp, +}; + +//call this function to test controller +// sendUserOtp('1','test@example.com') diff --git a/controllers/organizationController.js b/controllers/organizationController.js index 5a51e373..5206b039 100644 --- a/controllers/organizationController.js +++ b/controllers/organizationController.js @@ -3,10 +3,11 @@ const Organization = require('../models/organization.model'); const LunchWallet = require('../models/org_lunch_wallet.model'); const { createCustomError } = require('../errors/custom-errors'); const orgInvites = require('../models/organisation_invite.model'); -const { - generateUniqueToken, - sendInvitationEmail, -} = require('../utils/sendOTP'); + +const { generateUniqueToken, sendInvitationEmail } = { + generateUniqueToken: '', + sendInvitationEmail: '', +}; // Create a new organization and user (Admin user only) const createOrganization = async (req, res, next) => { @@ -40,7 +41,7 @@ const createOrganization = async (req, res, next) => { } }; -async function sendInvitation(req, res, next) { +async function sendInvite(req, res, next) { try { const { email, organizationId } = req.body; @@ -67,4 +68,4 @@ async function sendInvitation(req, res, next) { } } -module.exports = { sendInvitation, createOrganization }; +module.exports = { sendInvite, createOrganization }; diff --git a/controllers/userController.js b/controllers/userController.js index b9740e37..afdcd09a 100644 --- a/controllers/userController.js +++ b/controllers/userController.js @@ -1,9 +1,8 @@ /* eslint-disable camelcase */ -const bcrypt = require('bcrypt'); -const User = require('../models/user.model'); -const Invite = require('../models/organisation_invite.model'); +const User = require('../models/user.model'); //import user model +const { createCustomError } = require('../errors/custom-errors'); -async function getMe(req, res) { +async function getMe(req, res, next) { try { const user = await User.findOne({ where: { id: req.user.id } }); @@ -23,17 +22,13 @@ async function getMe(req, res) { } } -async function getUserById(req, res) { +async function getUserById(req, res, next) { try { const userId = req.params.id; const user = await User.findOne({ where: { id: userId } }); if (!user) { - return res.status(404).json({ - success: false, - message: 'User not found', - data: null, - }); + throw createCustomError('User not found', 404); } res.status(200).json({ @@ -44,102 +39,12 @@ async function getUserById(req, res) { }, }); } catch (error) { - return res.status(500).json({ - success: false, - message: 'Internal Server Error', - data: null, - }); + next(error); } } // Controllers Function to register new user -async function createUser(req, res) { - try { - const { - first_name, - last_name, - email, - phone, - password, - is_admin, - profile_pic, - org_id, - launch_credit_balance, - refresh_token, - bank_code, - bank_name, - bank_number, - token, - } = req.body; - - // Validate input data - if (!first_name || !last_name || !email || !password || !token) { - return res.status(400).json({ - success: false, - message: 'Missing required fields', - data: null, - }); - } - - // Check if the token is valid and retrieve org_id - const invite = await Invite.findOne({ where: { token } }); - - if (!invite || new Date() > invite.ttl) { - return res.status(400).json({ - success: false, - message: 'Invalid or expired invitation token', - data: null, - }); - } - - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - - const user = { - first_name, - last_name, - email, - phone, - password_hash: hashedPassword, - is_admin, - profile_pic, - org_id, - launch_credit_balance, - refresh_token, - bank_code, - bank_name, - bank_number, - }; - - const newUser = await User.create(user); - delete newUser.password_hash; - - res.status(200).json({ - success: true, - message: 'User registered successfully', - data: { - user: newUser, - }, - }); - } catch (error) { - if (error.name === 'SequelizeUniqueConstraintError') { - // Unique constraint violation (duplicate email) - return res.status(400).json({ - success: false, - message: 'Email already exists', - data: null, - }); - } - - return res.status(500).json({ - success: false, - message: error.message, - data: null, - }); - } -} - -async function getAllUsers(req, res) { +async function getAllUsers(req, res, next) { try { const users = await User.findAll({ where: { org_id: req.user.org_id }, @@ -160,7 +65,7 @@ async function getAllUsers(req, res) { }); } } -async function deleteUser(req, res) { +async function deleteUser(req, res, next) { try { const userId = req.params.id; @@ -191,7 +96,7 @@ async function deleteUser(req, res) { } } -async function updateUser(req, res) { +async function updateUser(req, res, next) { try { const userId = req.params.id; const { @@ -244,7 +149,6 @@ async function updateUser(req, res) { module.exports = { getMe, getUserById, - createUser, getAllUsers, updateUser, deleteUser, diff --git a/controllers/userLoginController.js b/controllers/userLoginController.js deleted file mode 100644 index 6eff515a..00000000 --- a/controllers/userLoginController.js +++ /dev/null @@ -1,38 +0,0 @@ -const jwt = require('jsonwebtoken'); -const bcrypt = require('bcrypt'); -const User = require('../models/user.model'); -const { createCustomError } = require('../errors/custom-errors'); - -const secretKey = process.env.JWT_SECRET_KEY; - -const loginController = async (req, res) => { - const { email, password } = req.body; - - try { - const user = await User.findOne({ where: { email } }); - - if (!user) { - return res.status(401).json({ message: 'Invalid credentials' }); - } - - const isPasswordValid = await bcrypt.compare(password, user.password_hash); - - if (!isPasswordValid) { - throw createCustomError('Invalid credentials', 401); - } - - const token = jwt.sign({ id: user.id }, secretKey, { - expiresIn: '1h', - }); - - res.status(200).json({ - success: true, - message: 'User logged in successfully', - data: { user, token }, - }); - } catch (error) { - res.status(500).json({ message: 'Internal server error' }); - } -}; - -module.exports = loginController; diff --git a/controllers/userLogoutController.js b/controllers/userLogoutController.js deleted file mode 100644 index cd205d61..00000000 --- a/controllers/userLogoutController.js +++ /dev/null @@ -1,19 +0,0 @@ -const jwt = require('jsonwebtoken'); -const { createCustomError } = require('../errors/custom-errors'); - -const logoutController = (req, res) => { - try { - const token = req.header('Authorization').replace('Bearer ', ''); - - jwt.verify(token, process.env.JWT_SECRET_KEY); - if (!token) { - createCustomError('User is not logged in.', 401); - } - - return res.status(200).json({ message: 'User logged out successfully.' }); - } catch (error) { - return res.status(200).json({ message: 'User logged out successfully.' }); - } -}; - -module.exports = logoutController; diff --git a/db/db.js b/db/db.js index b45933ce..2425066a 100644 --- a/db/db.js +++ b/db/db.js @@ -5,4 +5,11 @@ const URI = process.env.MYSQL_ADDON_URI; const sequelize = new Sequelize(URI, { dialect: 'mysql', logging: false }); +//The below is for local connection + +// const sequelize = new Sequelize('free_lunch_db', 'root', '64632120', { +// host: 'localhost', +// dialect: 'mysql' +// }); + module.exports = sequelize; diff --git a/middlewares/auth.js b/middlewares/auth.js index c06ea4ca..ce634e76 100644 --- a/middlewares/auth.js +++ b/middlewares/auth.js @@ -1,5 +1,6 @@ const jwt = require('jsonwebtoken'); const User = require('../models/user.model'); +const { createCustomError } = require('../errors/custom-errors'); async function auth(req, res, next) { try { @@ -11,14 +12,13 @@ async function auth(req, res, next) { if (!user) { //this should be updated after custom errors have been implemented - throw new Error('User not Authenticated'); + throw createCustomError('Access Denied', 401); } req.user = user.dataValues; req.token = token; } catch (error) { - console.log(error.message); - //switch to next(error) after error middleware have been created + next(error); } } diff --git a/middlewares/error-handler.js b/middlewares/error-handler.js index 0cdcb34a..23eb17ce 100644 --- a/middlewares/error-handler.js +++ b/middlewares/error-handler.js @@ -1,6 +1,7 @@ -const { CustomErrorClass } = require('../Errors/custom-errors'); +const { CustomErrorClass } = require('../errors/custom-errors'); const errorHandlerMiddleware = (err, req, res, next) => { + console.log(err.message); if (err instanceof CustomErrorClass) { return res .status(err.statusCode) diff --git a/models/lunches.model.js b/models/lunches.model.js index f57477d9..608445ec 100644 --- a/models/lunches.model.js +++ b/models/lunches.model.js @@ -42,13 +42,13 @@ const Lunch = sequelize.define( { tableName: 'lunches', createdAt: 'created_at', updatedAt: false }, ); // foreign key to user from receiver to allow usage like -// launch.user +// lunch.user Lunch.belongsTo(User, { foreignKey: 'recieverId', }); // foreign key to user from sender to allow usage like -// launch.user +// lunch.user Lunch.belongsTo(User, { foreignKey: 'senderId', }); diff --git a/models/organization.model.js b/models/organization.model.js index d1dbfe63..cd160b18 100644 --- a/models/organization.model.js +++ b/models/organization.model.js @@ -16,7 +16,7 @@ const Organization = sequelize.define( }, lunch_price: { type: DataTypes.DECIMAL, - defaultValue: 2000, + defaultValue: 1000, allowNull: false, }, currency_code: { diff --git a/models/user.model.js b/models/user.model.js index 4122b1ae..0b3cc429 100644 --- a/models/user.model.js +++ b/models/user.model.js @@ -36,6 +36,7 @@ const User = sequelize.define( }, is_admin: { type: DataTypes.BOOLEAN, + defaultValue: false, }, profile_pic: { type: DataTypes.STRING, @@ -44,7 +45,7 @@ const User = sequelize.define( type: DataTypes.UUID, references: { model: Organization, key: 'id' }, }, - launch_credit_balance: { + lunch_credit_balance: { type: DataTypes.INTEGER, defaultValue: 0, }, @@ -60,6 +61,17 @@ const User = sequelize.define( bank_number: { type: DataTypes.STRING, }, + bank_region: { + type: DataTypes.STRING, + }, + currency_code: { + type: DataTypes.STRING, + defaultValue: 'NGN', + }, + currency: { + type: DataTypes.STRING, + defaultValue: 'Naira', + }, }, { tableName: 'users', @@ -79,4 +91,7 @@ User.prototype.toJSON = function () { return values; }; +(async () => { + await User.sync({ alter: true }); +})(); module.exports = User; diff --git a/routes/auth.route.js b/routes/auth.route.js new file mode 100644 index 00000000..408c57e3 --- /dev/null +++ b/routes/auth.route.js @@ -0,0 +1,14 @@ +const express = require('express'); +const { + createUser, + loginUser, + logoutUser, +} = require('../controllers/authController'); + +const router = express.Router(); + +router.post('/auth/signup', createUser); +router.post('/auth/login', loginUser); +router.post('/auth/logout', logoutUser); + +module.exports = router; diff --git a/routes/orgRoutes.js b/routes/orgRoutes.js index bd3ee6bf..4877c669 100644 --- a/routes/orgRoutes.js +++ b/routes/orgRoutes.js @@ -1,8 +1,12 @@ const express = require('express'); const router = express.Router(); -const { createOrganization } = require('../controllers/organizationController'); +const { + createOrganization, + sendInvite, +} = require('../controllers/organizationController'); router.post('/create', createOrganization); +router.post('send-invite', sendInvite); module.exports = router; diff --git a/routes/users.js b/routes/users.js index ee7fb43a..8c25e2e4 100644 --- a/routes/users.js +++ b/routes/users.js @@ -4,17 +4,11 @@ const router = express.Router(); const { getMe, getUserById, - createUser, getAllUsers, updateUser, deleteUser, } = require('../controllers/userController'); -const loginController = require('../controllers/userLoginController'); -const logoutController = require('../controllers/userLogoutController'); -router.post('/auth/signup', createUser); -router.post('/auth/login', loginController); -router.post('/auth/logout', logoutController); router.get('/users/me', getMe); router.get('/users/:id', getUserById); router.get('/users/', getAllUsers);