diff --git a/.gitignore b/.gitignore index 3c3629e647..467d222a96 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules +/node_modules +/config/public.key.json +/config/private.key.json +.env \ No newline at end of file diff --git a/app/config/api_implementation.js b/app/config/api_implementation.js new file mode 100644 index 0000000000..6353281dca --- /dev/null +++ b/app/config/api_implementation.js @@ -0,0 +1,27 @@ +import axios from 'axios'; + +// Function to search events using Eventbrite API +async function searchEvents(searchTerm) { + const response = await axios.get(`https://www.eventbriteapi.com/v3/events/search/?q=${searchTerm}&token=YOUR_EVENTBRITE_API_KEY`); + return response.data.events; +} + +// Function to search conferences using Conferize API +async function searchConferences(searchTerm) { + const response = await axios.get(`https://api.conferize.com/v1/conferences?search=${searchTerm}`); + return response.data; +} + +// Function to search concerts using Songkick API +async function searchConcerts(searchTerm) { + const response = await axios.get(`https://api.songkick.com/api/3.0/events.json?apikey=YOUR_SONGKICK_API_KEY&query=${searchTerm}`); + return response.data.resultsPage.results.event; +} + +// Function to search travel locations using TripAdvisor API (Geoservices) +async function searchTravelLocations(searchTerm) { + const response = await axios.get(`https://api.tripadvisor.com/api/partner/2.0/location_search/${searchTerm}?key=YOUR_TRIPADVISOR_API_KEY`); + return response.data.data; +} + +export default {searchTravelLocations, searchConcerts, searchEvents, searchConferences} \ No newline at end of file diff --git a/app/config/database.js b/app/config/database.js new file mode 100644 index 0000000000..3ba40d898a --- /dev/null +++ b/app/config/database.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; + +const dbConn = async (req, res) => { + try { + // const connect = await mongoose.connect(process.env.ONLINE_DB); + // const connect = await mongoose.connect(process.env.DB_CONNECTION||process.env.ONLINE_DB); + const connect = await mongoose.connect(process.env.ONLINE_DB); + console.log(`Established connection to database ${connect.connection.host}`), connect.connection.name; + } catch (err) { + console.error(err); + } +}; + +export default dbConn; \ No newline at end of file diff --git a/app/config/mailer.js b/app/config/mailer.js new file mode 100644 index 0000000000..86e565ed67 --- /dev/null +++ b/app/config/mailer.js @@ -0,0 +1,68 @@ +import nodemailer from 'nodemailer'; +import mailGen from 'mailgen'; +import dotenv from 'dotenv' + +dotenv.config(); + +const transporter = nodemailer.createTransport({ + // host: "smtp.forwardemail.net", + host: "smtp.zoho.com", + service: "Zoho", + port: 465, + secure: true, + auth: { + // TODO: replace `user` and `pass` values from + user: process.env.MAILER_USERNAME, + pass: process.env.MAILER_PASSWORD, + } +}); + +let mailGenerator = new mailGen({ + theme: "cerberus", + product: { + name: "TripMatch Experience", + link: "https://tripmatch.com", + logo: "" + } +}); + +// async..await is not allowed in global scope, must use a wrapper +export default async function main(params) { + + var email = { + body: { + name: params.toName || params.toEmail, + intro: params.message || 'Welcome to Mailgen! We\'re very excited to have you on board.', + action: { + instructions: 'To get started with TripMatch, please click here:', + button: { + color: '#22BC66', // Optional action button color + text: 'Confirm your account', + link: params.link + } + }, + outro: 'Need help, or have questions? Just reply to this email, we\'d love to help.' + } + }; + + var emailBody = mailGenerator.generate(email); + // send mail with defined transport object + const info = await transporter.sendMail({ + from: "TripMatch Team, " + process.env.MAILER_USERNAME, // sender address + to: params.toEmail, // list of receivers + subject: params.subject, // Subject line + text: params.message, // plain text body + html: `${emailBody}`, // html body + }); + + console.log("Message sent: %s", info.messageId); + // Message sent: + + // + // NOTE: You can go to https://forwardemail.net/my-account/emails to see your email delivery status and preview + // Or you can use the "preview-email" npm package to preview emails locally in browsers and iOS Simulator + // + // +} + +// main().catch(console.error); \ No newline at end of file diff --git a/app/config/private.key.js b/app/config/private.key.js new file mode 100644 index 0000000000..707cf47392 --- /dev/null +++ b/app/config/private.key.js @@ -0,0 +1,17 @@ +export default { + PR_KEY:`-----BEGIN RSA PRIVATE KEY----- + MIICWwIBAAKBgHDKzzEz/Ej6atPybcUpSg0HNIv6ZMv7RVcA1RRSSlppwEazibx7 + 5fBG7W0Q5METsN8i9qQCxGPmfrTJCselDOoB4OY3jfwN18NOUVWdFzHYxpzIP0gp + MXFSzJho1+HUKqqdclmrluE+MfnRwhvHfXM/2oW6oUepW6buef16/w/XAgMBAAEC + gYAC5DALgtmkxUaXyE8oDrbnPpgKPpD26aoNYOgVbUaaqrtTFKcjPcnXLvpHIXe7 + u1V9YtgPvEJsjSwSVlUAaSq7jBISq7tyk7L/5ZxK0v2681r3BPe19s0j4z12sk32 + iMYIGnbqd8SMILQPJpexVuO8v8ku5MYjn1qooBLMUA5OgQJBAM2CFWRRLZBd/upT + h5KQyEb02kKbNGbnH+//k8/H8rdb8zM4CKvBQVvfsE//b4EMEV3V0p6NGnPvn4i+ + YvAr/gUCQQCMgSQOCd0/rGtOoRdLDNI+BdEpL9IoN59Y17G87HQuZb6nVGJEQE6g + duF712g5epSLx/LPuyAuXp9crYaaReErAkA1VT0X+7lHbh1p0qOsdcaEI6dBAeZn + jjPLpRhHJCzpSQOklzYo3M6JeVPYbwIRC4C2SXePji7/R4CvjDlA+ynBAkACPy5s + awv2sLMmlrzgnlveUgl+Nx2NNxZ9PTXCZ6WT+FyMIHVR0hVvy3bfnBo2kRc/BPuF + BuEE3M5/lObKIMPxAkEAzWDV6Ty7XzlfCMyYcJkryTUg1z00Q5RD59gRUbgnb393 + aDPxI31pb8hHKX2OvADyG0TU75YaoIofGGPg7roZ9g== + -----END RSA PRIVATE KEY-----` +} \ No newline at end of file diff --git a/app/config/public.key.js b/app/config/public.key.js new file mode 100644 index 0000000000..8e602f3259 --- /dev/null +++ b/app/config/public.key.js @@ -0,0 +1,7 @@ +export default { + PU_KEY:` + MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHDKzzEz/Ej6atPybcUpSg0HNIv6 + ZMv7RVcA1RRSSlppwEazibx75fBG7W0Q5METsN8i9qQCxGPmfrTJCselDOoB4OY3 + jfwN18NOUVWdFzHYxpzIP0gpMXFSzJho1+HUKqqdclmrluE+MfnRwhvHfXM/2oW6 + oUepW6buef16/w/XAgMBAAE=` +} \ No newline at end of file diff --git a/app/controller/auth.controllers.js b/app/controller/auth.controllers.js new file mode 100644 index 0000000000..2971ef2955 --- /dev/null +++ b/app/controller/auth.controllers.js @@ -0,0 +1,200 @@ +import mongoose from 'mongoose'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import otpGenerator from 'otp-generator'; +import userModel from '../models/users.model.js'; +import secrete from '../config/public.key.js'; +import mailer from '../config/mailer.js'; + + +//Sign up user +const signUpUser = async (req, res, next) => { + let userExists; + const {email, password, confirmPassword} = req.body; + + if (!email || !password || !confirmPassword) { + res.status(403).json({message: "All fields are required"}); + } else if (!password === confirmPassword) { + res.status(200).json({message: "Both passwords do not match"}); + } + + try { + userExists = await userModel.findOne({email}); + } catch (err) { + console.error(err.message); + } + + if (userExists) { + res.status(200).json({message: 'User already exists'}); + } + const salt = await bcrypt.genSalt(12); + const hashedPassword = await bcrypt.hash(password, salt); + const user = new userModel({ + email, + password: hashedPassword + }); + + try { + await user.save(); + } catch(err) { + res.status(404).json({message: err.message}); + } + + if (!user) { + res.status(500).json({message: 'Unable to complete registration'}); + } + + const message = `Thank you for signing up on TripMatch, please click the link bellow to verify your email`; + const params = { + 'toEmail': email, + 'subject': "TripMatch Registration Notification", + 'message': message, + 'toName': email, + 'link': `http://localhost:9900/api/auth/verify_email/?token=${user._id}&isSet=true` + } + const sendMail = await mailer(params); + console.log(sendMail); + res.status(200).json({message: 'Registration successful, A verification email has been sent to your email address, please verify your email address', data: {id: user._id, email: user.email}}); +}; + +//Login controller +const signInUser = async (req, res, next) => { + let user, data; + const {email, password} = req.body; + try { + user = await userModel.findOne({email}); + // const {password, updatedAt, ...dataOb} = user._doc; + data = user._doc; + const validPassword = await bcrypt.compare(password, data.password); + if (!validPassword) { + res.status(403).json({message: 'Invalid password'}); + } + + const jwtToken = await jwt.sign({ + userId: data._id, + userEmail: data.email, + }, secrete.PU_KEY, {expiresIn: '1800s'}); + res.status(200).json({message: "Login successful", token: jwtToken}); + } catch (err) { + res.status(403).json(err.message); + } +}; + +const verifyUser = async (req, res, next) => { + const { email} = req.body; + // const { email, password } = req.method = "GET" ? req.query : req.body; + try { + if (!email) { + res.status(403).json({message: "The user's email is required"}); + } else { + const exists = await userModel.findOne({ email, isVerified: true }); + if (!exists) { + res.status(404).json({message: 'Unknown user or Unverified account'}); + } else { + next(); + } + } + } catch (error) { + res.status(404).json({message: 'Authentication failed'}); + } +}; + +const verifyUserEmail = async (req, res, next) => { + const { token, isSet} = req.query; + try { + if (!token) { + res.status(500).json({message: "Email verification failed"}); + } else { + const exists = await userModel.findOne({ _id: token }); + if (!exists) { + res.status(404).json({message: 'Unknown user'}); + } else { + const update = await userModel.findByIdAndUpdate(token,{isVerified: isSet}, {new: true}); + if (!update) { + res.status(500).json({message:"Email verification Failed"}); + } else { + res.status(201).json({message: "Your email has been successfully verified"}); + } + } + } + } catch (error) { + res.status(404).json({message: 'Email verification failed'}); + } +}; + +const generateOTP = async (req, res, next) => { + req.app.locals.OTP = await otpGenerator.generate(6, {lowerCaseAlphabets:false, upperCaseAlphabets :false, specialChars:false}); + const message = `

You received this email because you requested for a password reset.


+ Use the OTP bellow to reset your password

${req.app.locals.OTP}

`; + const params = { + 'toEmail': req.body.email, + 'subject': "TripMatch Registration Notification", + 'message': message, + 'toName': req.body.email + } + const sendMail = await mailer(params); + let msg = "An OTP has been sent to your email address " + sendMail; + res.status(200).json({message: msg}); +}; + +const verifyOTP = async (req, res, next) => { + const {code} = req.body; + if (parseInt(req.app.locals.OTP) === parseInt(code)) { + req.app.locals.OTP = null; + req.app.locals.resetSession = true; + // res.status(200).json({message: "OTP is valid"}); + next(); + } else { + res.status(400).json({message: "Invalid OTP"}); + } +} + +const createResetSession = async (req, res) => { + if (req.app.locals.resetSession) { + req.app.locals.resetSession = false; + res.status(200).json({message: "Reset code is sent to your email address"}); + } else { + res.status(400).json({message: "Session expired"}); + } +} + +const resetUserPassword = async (req, res) => { + const { email, password } = req.body; + const salt = await bcrypt.genSalt(12); + const hashedPassword = await bcrypt.hash(password, salt); + if (!req.app.locals.resetSession) { + res.status(500).json({message: 'Invalid session'}); + } else { + try { + try { + await userModel.findOne({ email }) + .then(user =>{ + userModel.findByIdAndUpdate(user._id, {password:hashedPassword},{new: true}) + .then(result => { + res.status(200).json({message: "Password reset successful "}); + }) + .catch(err => + res.status(404).json({message: "Unable to reset password " + result.error}) + ) + }) + .catch(err => { + res.status(404).json({message: "Unknown email address"}); + }); + } catch (err) { + res.status(500).json({err}); + } + } catch (error) { + res.status(400).json({ error: error }); + } + } +} + +function localVariables (req, res, next) { + req.app.locals = { + OTP: null, + resetSession: false, + } + next(); +} + +export default {signUpUser, signInUser, verifyUser, localVariables, generateOTP, verifyOTP, createResetSession, resetUserPassword, verifyUserEmail}; \ No newline at end of file diff --git a/app/controller/trip.controllers.js b/app/controller/trip.controllers.js new file mode 100644 index 0000000000..1f21ba3ea8 --- /dev/null +++ b/app/controller/trip.controllers.js @@ -0,0 +1,411 @@ +import jwt from 'jsonwebtoken'; +import tripModel from '../models/trips.model.js'; +import User from '../models/users.model.js'; +import Requests from '../models/request.model.js'; +import secrete from '../config/public.key.js'; +import mailer from '../config/mailer.js'; + +const validateJwtToken = async (req, res, next) => { + // const { token } = req.body || req.query; + if (!req.headers.authorization) { + res.status(500).json({message: "Un Authorized request"}); + } else { + const token = req.headers.authorization.split(" ")[1]; + try { + if (!token) { + res.status(401).json({ message: "Invalid token"}); + } else { + const validateToken = await jwt.verify(token, secrete.PU_KEY); + req.user = validateToken; + next(); + } + } catch (error) { + res.status(500).json({message: error.message}); + } + } +}; + +//Get a single post +const getATrip = async (req, res) => { + try { + const trip = await tripModel.findById(req.params.id).sort({createdAt: "descending"}); + if (!trip) { + res.status(404).json({message: "May be this trip has been removed!"}); + } + res.status(200).json(trip); + }catch(err) { + res.status(500).json({error: err.message}); + } +}; + +//Get all post by a user +const getUserTrips = async(req, res, next)=> { + try { + const currentUser = await User.findById(req.user.userId).sort({createdAt: "descending"}); + const userTrips = await tripModel.find({userId: currentUser._id.toString()}); + // console.log(userTrips, currentUser._id.toString()); + const friendTrips = await Promise.all( + currentUser.following.map((friendId) => { + return tripModel.find({userId: friendId}) + }) + ); + const Trips = userTrips.concat(...friendTrips); + if (!Trips) { + res.status(404).json({message: "No trips found"}); + } else { + res.status(200).json({message: 'Successfully retrieved', data: Trips}); + } + } catch (err) { + res.status(500).json({error: err.message}); + } +} + +const getAllUserTrips = async (req, res) => { + try { + const user = await User.findOne({_id: req.user.userId}).sort({createdAt: "descending"}); + console.log(user); + const trips = await tripModel.find({userId: user._id}); + res.status(200).json(trips); + } catch (err) { + res.status(500).json({error: err.message}); + } +}; + +const getTaggedTrips = async (req, res) => { + const userId = req.user.userId; + try { + const trips = await tripModel.find(); + let myTrips = []; + let tripMap = trips.map(async(v, nx)=>{ + if (v._doc.requests.includes(userId) && v._doc.isPublic === false) { + const user = await User.findOne({_id: v._doc.userId}); + const {id, _v, createdAt, updatedAt, password, isAdmin, coverPhoto, ...others} = user._doc; + v._doc.createdBy = others; + myTrips.push(v._doc); + } else { + const user = await User.findOne({_id: v._doc.userId}); + const {id, _v, createdAt, updatedAt, password, isAdmin, coverPhoto, ...others} = user._doc; + v._doc.createdBy = others; + myTrips.push(v._doc); + } + return myTrips; + }); + const updatedTrips = await Promise.all(tripMap); + res.status(200).json(updatedTrips); + } catch (err) { + res.status(400).json({message: err.message}); + } +}; + +const getPublicTrips = async (req, res) => { + + try { + const trips = await tripModel.find(); + let tripMap = trips.map(async(v, nx)=>{ + const user = await User.findOne({_id: v._doc.userId}); + const {id, _v, createdAt, updatedAt, password, isAdmin, coverPhoto, ...others} = user._doc; + v._doc.createdBy = others; + return v._doc; + }); + const updatedTrips = await Promise.all(tripMap); + const publicTrips = updatedTrips.filter(trip => trip.isPublic === true); + res.status(200).json(publicTrips); + } catch (err) { + res.status(500).json({error: err.message}); + } + + // try { + // const trips = await tripModel.find(); + // let tripMap = []; + // trips.forEach(async(v, nx)=>{ + // const user = await User.findOne({_id: v._doc.userId}); + // const {_id, __v, createdAt, updatedAt, password, isAdmin, coverPhoto, ...others} = user._doc; + // v._doc.createdBy = others; + // console.log(v._doc); + // return v._doc; + // }); + // const publicTrips = trips.filter(trip => trip.isPublic === true); + // res.status(200).json(publicTrips); + // } catch (err) { + // res.status(500).json({error: err.message}); + // } +}; + +/** + * Create new Trip + * link: http://localhost:9900/api/trips/create_new_trip + * @params: { + * userId : "string", + * tripName : "string", + * tripMembers: "array", + * requests: "array", + * tripType: "string", + * + * } + */ +const createNewTrip = async (req, res) => { + let userExists; + try { + userExists = await User.findById(req.user.userId); + if(!userExists) { + res.status(403).json({message: "You must be logged in to create a trip"}); + } else { + if (req.user.userId) { + const data = req.body; + data.userId = req.user.userId + const newTrip = new tripModel(data) + const saveTrip = await newTrip.save(); + res.status(200).json(saveTrip); + } else { + res.status(403).json({message: "You must create a trip only with your account"}); + } + } + } catch (err) { + res.status(404).json({ error: err.message}); + } +} + +const sendTripInvites = async (req, res) => { + const { tripId, users } = req.body; + const userId = req.user.userId; + let failedTag = [] + try{ + const trip = await tripModel.findById(tripId); + if (!trip) { + res.status(404).json({ message: 'This trip may have expired or cancelled'}); + } else { + for (let i=0; i < users.length; i++) { + if (!trip.requests.includes(users[i])) { + const tags = await trip.updateOne({$push:{requests: users[i]}}); + if (!tags) { + const user = await User.findOne({_id: users[i]}); + const failed = {name: user.lastName + " " + user.firstName, msg: "Could not tag this person"}; + failedTag.push(failed); + } else { + const user = await User.findOne({_id: users[i]}); + const message = `

You have been invited to join this trip


${trip.tripName}`; + const params = { + 'toEmail': user.email, + 'subject': "TripMatch Registration Notification", + 'message': message, + 'toName': user.email + } + const sendMail = await mailer(params); + } + } else { + const user = await User.findOne({_id: users[i]}); + const name = user.lastName + " " + user.firstName; + const failed = {name:name, msg: "Could not tag this person"}; + failedTag.push(failed); + } + } + if (failedTag.length == users.length) { + res.status(403).json({message: "Could not tag any of your selections"}); + } else if (failedTag.length > 0 && failedTag.length < users.length) { + res.status(200).json({message: "Some persons were not tagged", failedTag}); + } else { + res.status(200).json({message: "Selections tagged successfully", failedTag}); + } + } + } catch (err) { + res.status(401).json({ message: err.message}) + } +} + +const actOnTripInvites = async (req, res) => { + const { tripId } = req.body; + const userId = req.user.userId; + try { + const invite = await tripModel.findOne({_id: tripId}); + if (!invite) { + res.status(404).json({message: "No such Trip exists!"}); + } else { + if (!invite.tripMembers.includes(userId) && invite.requests.includes(userId)) { + await invite.updateOne({$push:{tripMembers: userId}}); + await invite.updateOne({$pull:{requests: userId}}); + const creator = await User.findOne({_id: invite.userId}); + const user = await User.findOne({_id: userId}); + const fullName = user.lastName + " " + user.firstName; + const message = `

${fullName} has accepted to join this trip
${invite.tripName}`; + const params = { + 'toEmail': creator.email, + 'subject': "TripMatch Registration Notification", + 'message': message, + 'toName': creator.email + } + const sendMail = await mailer(params); + console.log(sendMail); + res.status(200).json({message: "You have accepted to join this trip "}); + } else { + await invite.updateOne({$pull:{tripMembers: userId}}); + await invite.updateOne({$push:{requests: userId}}); + const creator = await User.findOne({_id: invite.userId}); + const user = await User.findOne({_id: userId}); + const fullName = user.lastName + " " + user.firstName; + const message = `

${fullName} has declined your invitation to join this trip.
`; + const params = { + 'toEmail': creator.email, + 'subject': "TripMatch Invitation Notification", + 'message': message, + 'toName': creator.email + } + const sendMail = await mailer(params); + console.log(sendMail); + res.status(200).json({message: "You have declined this trip invite "}); + } + } + } catch (err) { + res.status(500).json({message: err}) + } +} + +/** + * Get Trip requests + * + */ +const getMyTripRequests = async (req, res) => { + const user = await User.findOne({_id: req.user.userId}); + if (!user) { + res.status(404).json({message: "You have to be logged in to access this records"}); + } else { + const request = await Requests.find({tripId: req.body.tripId}); + if (request.length > 0) { + let requestMap = request.map(async(v, nx)=>{ + const user = await User.findOne({_id: v._doc.requesterId}); + const {_id, __v, createdAt, updatedAt, password, isAdmin, coverPhoto, ...details} = user._doc; + v._doc.requester = details; + return v._doc; + }); + const updatedTrips = await Promise.all(requestMap); + res.status(200).json({message: "You have " + request.length + " on this trip", requests:updatedTrips}) + } else { + res.status(404).json({message: "No requests yet"}); + } + } +} + +/** + * Send request to join a trip + * @param {tripId, id} req + * @param {a} res + */ +const approveRequests = async (req, res) => { + let userExists; + const {tripId, requestId} = req.body; + try { + userExists = await User.findOne({_id: req.user.userId}); + if(!userExists) { + res.status(403).jsonOne({message: "You must be logged in to approve this request"}); + } else { + const trip = await tripModel.findOne({_id:tripId}); + if (trip) { + const request = await Requests.findOne({_id: requestId}); + if (!request) { + res.status(404).json({message: "It seems this request no longer exists"}); + } else { + const { requesterId, _id } = request._doc; + if (!trip.requests.includes(requesterId)) { + await trip.updateOne({$push:{requests: requesterId}}); + await Requests.updateOne({_id:requestId}, {isApproved: true}); + res.status(200).json({message: "Your have accepted this request ", request}); + } else { + await trip.updateOne({$pull:{requests: requesterId}}); + await Requests.updateOne({_id:requestId}, {isApproved: false}); + res.status(200).json({message: "You have declined this request", request}); + } + } + } else { + res.status(404).json({message: "May be this trip has expired or removed"}); + } + } + } catch (err) { + res.status(404).json({ error: err.message}); + } +} + +// Send trip request +const sendTripRequest = async (req, res) => { + let userExists; + const {tripId, isGroup, others} = req.body; + try { + userExists = await User.findOne({_id: req.user.userId}); + + if(!userExists) { + res.status(403).json({message: "You must be logged in to send a request"}); + } else { + const trips = await tripModel.findOne({_id: tripId}); + if (trips) { + const { _id } = trips._doc; + + const requests = await Requests.findOne({tripId: _id.toString()}); + + // If the trip does not exist any + if (!requests) { + const data = {requesterId:req.user.userId, tripId: _id.toString(), isGroup: isGroup, others: others} + const newRequest = new Requests(data); + const saveRequest = await newRequest.save(); + res.status(200).json({message: "Your request has been sent ", saveRequest}); + } else if (requests.isApproved) { + return res.status(409).json({ message: 'This request is already accepted' }); + } else { + await Requests.deleteOne({_id: requests._doc._id}); + res.status(200).json({message: "You have withdrawn your request", requests}); + } + } else { + res.status(404).json({message: "May be this trip has expired or removed"}); + } + } + } catch (err) { + res.status(404).json({ error: err.message}); + } +} + +//Update a post +const updateTrip = async (req, res) => { + try { + const trip = await tripModel.findById(req.params.id); + if (!trip.userId == req.params.id) { + res.status(403).json({message: "You cannot trip on a person's timeline without a user's consent."}); + } + await trip.updateOne({$set: req.body}); + res.status(200).json({message: "The trip was successfully updated."}); + } catch(err) { + res.status(403).json(err.message); + } +} + +//Delete a post +const deleteTrip = async (req, res) => { + try { + const trip = await tripModel.findById(req.params.id); + if (!trip.userId == req.params.id) { + res.status(403).json({message: "You cannot delete another person's trip without a user's consent."}); + } + await trip.deleteOne(); + res.status(200).json({message: "The trip was successfully deleted."}); + } catch(err) { + res.status(403).json(err.message); + } +} + +//Like a post +const likeTrip = async (req, res) => { + try { + const trip = await tripModel.findById(req.params.id); + if (trip) { + if (!trip.likes.includes(req.body.userId)) { + await trip.updateOne({$push:{likes: req.body.userId}}); + res.status(200).json({message: "Liked trip", trip}); + } else { + await trip.updateOne({$pull:{likes: req.body.userId}}); + res.status(200).json({message: "Unliked trip", trip}); + } + } else { + res.status(404).json({message: "May be this trip has been removed"}); + } + } catch(err) { + res.status(500).json(err.message); + } +} + +export default {getPublicTrips, getUserTrips, getATrip, createNewTrip, updateTrip, deleteTrip, likeTrip, getAllUserTrips, approveRequests, sendTripRequest, validateJwtToken, getTaggedTrips, getMyTripRequests, sendTripInvites, actOnTripInvites} \ No newline at end of file diff --git a/app/controller/user.controllers.js b/app/controller/user.controllers.js new file mode 100644 index 0000000000..00f52783ad --- /dev/null +++ b/app/controller/user.controllers.js @@ -0,0 +1,193 @@ +import usersModel from "../models/users.model.js"; +import jwt from "jsonwebtoken"; + +import secrete from "../config/public.key.js"; +import apis from "../config/api_implementation.js"; + +const validateJwtToken = async (req, res, next) => { + // const { token } = req.body || req.query; + const token = req.headers.authorization.split(" ")[1]; + try { + if (!token) { + res.status(401).json({ message: "Invalid token"}); + } else { + const validateToken = await jwt.verify(token, secrete.PU_KEY); + req.user = validateToken; + next(); + } + } catch (error) { + res.status(500).json({message: error.message}); + } +}; + +const getAllUsers = async (req, res, next) => { + try { + const users = await usersModel.find(); + if(!users) { + res.status(404).json({message: "No users available"}); + } + const {password, updatedAt, ...other} = users; + // const {updatedAt, ...other} = users._object; + res.status(200).json({other}); + } catch (err) { + res.status(400).json({message: err.message}); + } +}; + +//Get followers by user id. +const getFollowers = async (req, res) => { + const userId = req.params.userId; + + try { + const user = await usersModel.findById(userId); + if (user) { + const friends = await Promise.all( + user.following.map(friendId =>{ + return usersModel.findById(friendId); + }) + ); + if (friends) { + const userFriends = []; + friends.map(friend=>{ + const {_id, userName, profilePhoto} = friend; + userFriends.push({_id, userName, profilePhoto}); + }) + res.status(200).json(userFriends); + } else { + res.status(404).json({message: 'You have no friends yet.', friends}); + } + } else { + res.status(404).json({message: 'User not found', user}); + } + } catch (err) { + console.log(err) + res.status(500).json({message: err.message}); + } +} + +//Get a user by user id +const getUser = async (req, res) => { + const id = req.user.userId; + const userName = req.user.userEmail; + console.log(id, userName, req) + try { + const user = id ? await usersModel.findById(id) : await usersModel.findOne({email: userName}); + if (!user) { + res.status(404).json({message: 'User not found'}); + } else { + const {password, updatedAt, ...other} = user._doc; + res.status(200).json({other}); + } + } catch (err) { + console.error(err); + res.status(500).json({error: err}); + } +}; + +//Update user details +const updateUser = async (req, res, next) => { + const {firstName, lastName, otherName} = req.body; + const id = req.user.userId; + let userExists, updateUser; + if (!firstName || !lastName) { + res.status(404).json({message: "You cannot omit first name and last name"}); + } else { + try { + userExists = await usersModel.findById({_id:id}); + if (!userExists) { + res.status(404).json({message: "User does not exist"}); + } else { + updateUser = await usersModel.findByIdAndUpdate(id, req.body,{new: true}); + if (!updateUser) { + res.status(403).json({message: "Update failed"}); + } else { + res.status(200).json({message: "Update successful", updateUser}); + } + } + } catch (err) { + res.status(400).json({message: err}); + } + } +} + +//delete user +const deleteUser = async (req, res) => { + let userExists; + try { + userExists = await usersModel.findById(req.params.id); + } catch (err) { + res.status(404).json({ message: err}); + } + if(!userExists) { + res.status(404).json({ message: 'User not found' }); + } + await usersModel.findByIdAndRemove(req.params.id); + res.status(200).json(userExists); +}; + +//Follow user +const followUser = async (req, res) => { + if (req.body.userId !== req.params.id) { + try { + const user = await usersModel.findById(req.body.userId); + const currentUser = await usersModel.findById(req.params.id); + if(!user.followers.includes(req.params.id)) { + await user.updateOne({$push: {followers: req.params.id}}); + await currentUser.updateOne({$push: {following: req.body.userId}}); + res.status(200).json({message:"You are now follwing ", success: true, userinfo: user.userName}); + }else { + res.status(403).json({message: "You are already following this user."}); + } + } catch(err) { + res.status(500).json({error: err.message}); + } + } else { + res.status(403).json({message: "You can't follow yourself"}); + } +}; + +//Unfollow a user +const unfollowUser = async (req, res) => { + console.log(req.body.userId, req.params.id) + if (req.body.userId !== req.params.id) { + try { + const user = await usersModel.findById(req.body.userId); + const currentUser = await usersModel.findById(req.params.id); + if(user.followers.includes(req.params.id)) { + await user.updateOne({$pull: {followers: req.params.id}}); + await currentUser.updateOne({$pull: {following: req.body.userId}}); + res.status(200).json({message:"You are no longer follwing ", success: true, userinfo: user.userName}); + }else { + res.status(403).json({message: "You are not following this user."}); + } + } catch(err) { + res.status(500).json({message: err, error: err.message}); + } + } else { + res.status(403).json({message: "You can't unfollow yourself"}); + } +}; + + +const filterSearch = async (req, res) => { + const {searchTerm} = req.body.term; // Get the search term from user posts + try { + // Make API requests based on the search term + const events = await apis.searchEvents(searchTerm); + const conferences = await apis.searchConferences(searchTerm); + const concerts = await apis.searchConcerts(searchTerm); + const travelLocations = await apis.searchTravelLocations(searchTerm); + + // Combine and send the search results to the client + res.status(200).json({ + events, + conferences, + concerts, + travelLocations + }); + } catch (error) { + res.status(500).json("Internal Server Error " + error); + } +}; + +export default {getAllUsers, getUser, updateUser, deleteUser, followUser, unfollowUser, getFollowers, validateJwtToken, filterSearch}; \ No newline at end of file diff --git a/app/models/request.model.js b/app/models/request.model.js new file mode 100644 index 0000000000..72f5f6fbb9 --- /dev/null +++ b/app/models/request.model.js @@ -0,0 +1,28 @@ +import mongoose from 'mongoose' + + +const requestSchema = new mongoose.Schema({ + tripId: { + type: 'string', + required: true, + + }, + requesterId: { + type: String, + require: true, + }, + isGroup: { + type: Boolean, + default: false + }, + others: { + type: Array, + default: [] + }, + isApproved: { + type: Boolean, + default: false + } +}, {timestamps: true}); + +export default mongoose.model('Requests', requestSchema); \ No newline at end of file diff --git a/app/models/trips.model.js b/app/models/trips.model.js new file mode 100644 index 0000000000..bd7d630ebe --- /dev/null +++ b/app/models/trips.model.js @@ -0,0 +1,86 @@ +import mongoose from "mongoose"; + +const tripsSchema = new mongoose.Schema({ + userId: { + type: String, + require: true, + unique: false + }, + tripName: { + type: String, + require: true, + unique: false + }, + tripMembers: { + type: Array, + require: false, + default: [] + }, + requests: { + type: Array, + require: false, + default: [] + }, + teamSize: { + type: Number, + require: true, + }, + gender: { + type: Array, + require: false, + default: [] + }, + activities: { + type: Array, + require: false, + default: [] + }, + location: { + type: Array, + default: [] + }, + tripType: { + type: String, + require: false, + }, + tripDate: { + type: Number, + require: true, + }, + tripCost: { + type: Number, + require: true, + }, + ageRange: { + type: String, + require: false + }, + personalityDescription: { + type: String, + require: false, + min: 6 + }, + note: { + type: String, + require: false, + }, + tripImages: { + type: Array, + default: [] + }, + likes: { + type: Array, + default: [] + }, + comments: { + type: Array, + default: [] + }, + isPublic: { + type: Boolean, + require: true, + default: true, + } +}, {timestamps: true}); + +export default mongoose.model('Trip', tripsSchema); \ No newline at end of file diff --git a/app/models/users.model.js b/app/models/users.model.js new file mode 100644 index 0000000000..92de23f952 --- /dev/null +++ b/app/models/users.model.js @@ -0,0 +1,94 @@ +import mongoose from 'mongoose' + +const userSchema = new mongoose.Schema({ + userName: { + type: String, + required: false, + unique: false + }, + firstName: { + type: String, + required: false, + min: 3, + max: 255 + }, + lastName: { + type: String, + required: false, + min: 3, + max: 255 + }, + dateOfBirth: { + type: Number, + required: false + }, + otherName: { + type: String, + required: false, + min: 3, + max: 255 + }, + mobile: { + type: String, + required: false, + unique: true, + max: 16 + }, + email: { + type: String, + required: true, + unique: true, + max: 255 + }, + password: { + type: String, + required: true, + min: 6, + }, + profilePhoto: { + type: String, + default: '' + }, + coverPhoto: { + type: String, + default: '' + }, + followers: { + type: Array, + default: [], + }, + following: { + type: Array, + default: [], + }, + isVerified: { + type: Boolean, + default: false + }, + identification: { + type: String, + min: 6 + }, + city: { + type: String, + max: 255 + }, + town: { + type: String, + max: 255 + }, + state: { + type: String, + max: 255 + }, + from: { + type: String, + max: 255 + }, + relationship: { + type: Number, + enum: [1,2,3,4] + } +},{timestamps: true}); + +export default mongoose.model('User', userSchema); \ No newline at end of file diff --git a/app/routers/auth.routes.js b/app/routers/auth.routes.js new file mode 100644 index 0000000000..357955ec56 --- /dev/null +++ b/app/routers/auth.routes.js @@ -0,0 +1,27 @@ +import routes from 'express'; +import authControllers from '../controller/auth.controllers.js'; + +const authRoutes = routes.Router(); + + +//POST Requests +authRoutes.route('/user_sign_up').post(authControllers.signUpUser); //Sign up route /signup + +authRoutes.route('/verify_email').get(authControllers.verifyUserEmail); //Verify User Email + + +authRoutes.route('/user_sign_in').post(authControllers.verifyUser, authControllers.signInUser); //login route + +// authRoutes.route('/user_sign_out').post(authControllers.signOutUser).all(authControllers.isAuthenticated) //Log out Route + +//GET Requests + +authRoutes.route('/generate_otp').get(authControllers.verifyUser, authControllers.localVariables, authControllers.generateOTP); //Generate OTP + +authRoutes.route('/verify_otp').get(authControllers.verifyUser, authControllers.verifyOTP); //Verify OTP + + +//PUT Requests +authRoutes.route('/reset_user_password').put(authControllers.verifyUser, authControllers.verifyOTP, authControllers.resetUserPassword); //reset password route + +export default authRoutes; \ No newline at end of file diff --git a/app/routers/trip.routes.js b/app/routers/trip.routes.js new file mode 100644 index 0000000000..4cc2d4924a --- /dev/null +++ b/app/routers/trip.routes.js @@ -0,0 +1,41 @@ +import express from 'express'; +import tripController from '../controller/trip.controllers.js'; + + +const routers = express.Router(); + +//GET Requests +// routers.route('/:id').get(tripController.validateJwtToken, tripController.getATrip); //Get a single trip + +routers.route('/find_trips/').get(tripController.validateJwtToken, tripController.getUserTrips); //Get all friend trips + +routers.route('/get_trips/').get(tripController.validateJwtToken, tripController.getAllUserTrips); //Get all user's trips + +routers.route('/get_tagged_trips/').get(tripController.validateJwtToken, tripController.getTaggedTrips); //Get all tagged trips + +routers.route('/get_public_trips/').get(tripController.getPublicTrips); //Get all public trips + + +routers.route('/view_trip_requests').get(tripController.validateJwtToken, tripController.getMyTripRequests); //Get a single trip + + +//POST Requests +routers.route('/create_new_trip').post(tripController.validateJwtToken, tripController.createNewTrip); //create a new trip + +routers.route('/send_request/').post(tripController.validateJwtToken, tripController.sendTripRequest); //send +routers.route('/send_trip_invites/').post(tripController.validateJwtToken, tripController.sendTripInvites); //send trip invites + +routers.route('/respond_to_invites/').post(tripController.validateJwtToken, tripController.actOnTripInvites); //Act on trip invites + + +//PUT Requests +routers.route('/approve_request/').put(tripController.validateJwtToken, tripController.approveRequests); //approve a request to join a trip + +routers.route('/update_trip/:id').put(tripController.validateJwtToken, tripController.updateTrip); //Update a trip + +routers.route('/like_trip/:id').put(tripController.likeTrip); + + +//DELETE requests +routers.route('/delete_trip/:id').delete(tripController.validateJwtToken, tripController.deleteTrip); //Delete trip +export default routers; \ No newline at end of file diff --git a/app/routers/users.routes.js b/app/routers/users.routes.js new file mode 100644 index 0000000000..ad2b1dd667 --- /dev/null +++ b/app/routers/users.routes.js @@ -0,0 +1,27 @@ +import routes from 'express'; +import usersController from '../controller/user.controllers.js'; + +const routers = routes.Router(); + +//GET Requests +routers.route('/all_users/').get(usersController.validateJwtToken, usersController.getAllUsers); //get all users + +routers.route('/get_user/').get(usersController.validateJwtToken, usersController.getUser); //get a users + +routers.route('/get_friends/:userId').get(usersController.validateJwtToken, usersController.getFollowers); //Get friends of user + +routers.route('/search/').get(usersController.validateJwtToken, usersController.filterSearch) //Search and filter user terms + + + +//PUT Requests +routers.route('/update_user/').put(usersController.validateJwtToken, usersController.updateUser); //update user + +routers.route('/follow_user/').put(usersController.validateJwtToken, usersController.followUser); //Follow a user + +routers.route('/unfollow_user/').put(usersController.unfollowUser); //Unfollow a user + +//DELETE Requests +routers.route('/delete_user/:id').delete(usersController.validateJwtToken, usersController.deleteUser); //delete user + +export default routers \ No newline at end of file diff --git a/index.js b/index.js index afc6a8bd6c..703a6c267e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,47 @@ -const express = require('express') -const app = express() -app.all('/', (req, res) => { - console.log("Just got a request!") - res.send('Yo!') -}) -app.listen(process.env.PORT || 3000) \ No newline at end of file +import express from 'express'; +import morgan from 'morgan'; +import dotenv from 'dotenv'; +import cors from 'cors'; +// import bodyParser from 'body-parser'; + +import dbConn from './app/config/database.js'; +import authRoutes from './app/routers/auth.routes.js'; +import userRoutes from './app/routers/users.routes.js'; +import tripRoutes from './app/routers/trip.routes.js'; + +const app = express(); + +dotenv.config(); + +app.use(cors()); + +// initialize middlewares + +//define port +const port = process.env.PORT || 8090; + +app.use(express.json()); +app.use(morgan('combined')); +app.disable('x-powered-by'); + +app.use(express.urlencoded({ extended: true})); + +//authentication route +app.use('/api/auth', authRoutes); + +// app.use(function(req, res, next) { +// if (!req.headers.authorization) { +// return res.status(403).json({ error: 'You must be logged In' }); +// } +// next(); +// }); + +//define routers of the application +app.use('/api/users', userRoutes); + +//Trips routes +app.use('/api/trips', tripRoutes); + +dbConn().then(app.listen(port)) + .then(()=>console.log(`Connection to server established on port ${port}`)).catch((err)=>console.log(err)); + diff --git a/index2.js b/index2.js new file mode 100644 index 0000000000..6672e259ef --- /dev/null +++ b/index2.js @@ -0,0 +1,7 @@ +const express = require('express') +const app = express() +app.all('/', (req, res) => { + console.log("Just got a request!") + res.send('Dev Branch!') +}) +app.listen(process.env.PORT || 3000) diff --git a/package.json b/package.json index df8fc9c760..74aa6d522c 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,48 @@ "version": "1.0.0", "description": "", "main": "index.js", + "type": "module", "scripts": { "start": "node index.js" }, + "keywords": [ + "trip", + "match", + "travel", + "group", + "close", + "public", + "connect", + "tour" + ], "repository": { "type": "git", "url": "git+https://github.com/cyclic-software/starter-express-api.git" }, - "author": "", + "author": "Samuel Tamunobarainipiri\u001b[D\u001b[D\u001b[D\u001b[Joseph Tamunobarasinipiri", "license": "ISC", "bugs": { "url": "https://github.com/cyclic-software/starter-express-api/issues" }, "homepage": "https://github.com/cyclic-software/starter-express-api#readme", "dependencies": { - "express": "^4.18.2" + "axios": "^1.6.7", + "bcrypt": "^5.1.1", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^16.4.4", + "express": "^4.18.2", + "express-async-handler": "^1.2.0", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "mailgen": "^2.0.28", + "mongoose": "^8.1.3", + "morgan": "^1.10.0", + "nodemailer": "^6.9.10", + "nodemon": "^3.0.3", + "otp-generator": "^4.0.1", + "path": "^0.12.7", + "unique-random": "^3.0.0", + "uuid": "^9.0.0" } }