// This is the backend main file for the Fellowship social app. // It uses mongo as DB, and the connection url should be defined on the .env file // Most of the API endpoints require authentication, which is checked by sessionChecker function. // Load enviroment keys for stripe, mongo, vimeo, email require('dotenv').config(); const express = require('express'); const path = require('path'); const app = express(); const port = process.env.PORT || 3000; // Setup to run on heroku const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const cors = require('cors'); const Notifications = require("./notifications"); // Custom Notification Methods const webPush = require('web-push'); // TODO: we are probably missing a rate limiter here. const publicVapidKey = process.env.PUBLIC_VAPID_KEY; const privateVapidKey = process.env.PRIVATE_VAPID_KEY; const webPushEmail = process.env.WEB_PUSH_EMAIL; webPush.setVapidDetails('mailto:' + webPushEmail, publicVapidKey, privateVapidKey); var corsOptions = { origin: ['http://localhost:8080', "https://social.emmint.com", "https://fellowship.emmint.com"], credentials: true }; app.use(cors(corsOptions)); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser()); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const DB = require("./mongoDB.js"); // Utilities const getSessionId = function (req) { const session_id = req.cookies.session_id || req.query.session_id || req.body.session_id; return session_id; } const getUserId = function (req) { const user_sid = req.cookies.user_sid || req.query.user_sid || req.body.user_sid; return user_sid; } const getProfileId = function (req) { const profile_id = req.cookies.profile_id || req.query.profile_id || req.body.profile_id; return profile_id; } // Object Definitions const Post = require("./def/post.js") const Profile = require("./def/profile.js"); // Routes const profileRoute = require('./routes/profile.js'); const postRoute = require('./routes/post.js'); const songsRoute = require('./routes/songs.js'); const paymentsRoute = require('./routes/payments.js'); const subsplashRoute = require('./routes/subsplash.js'); const bibleRoute = require('./routes/bible.js'); // Get DB.getDB.then((DB) => { app.use(DB.logger) // middleware function to check for logged-in users const sessionChecker = async (req, res, next) => { const session_id = getSessionId(req); const user_sid = getUserId(req); let profile_id = getProfileId(req); if (session_id && user_sid) { const userInfo = await DB.checkSessionOnDB(session_id, user_sid); req.userInfo = userInfo; if (!await DB.getProfileCache(profile_id)) { const latestProfile = await DB.latestProfile(user_sid); res.cookie('profile_id', latestProfile._id, cookiesOptions); profile_id = latestProfile._id; } req.profileInfo = { _id: profile_id } if (!userInfo) return res.redirect('/login'); next(); } else { return res.redirect('/login'); } }; // route for Home-Page app.get('/', sessionChecker, async (req, res) => { if (req.userInfo) return res.json({ status: "ok", userInfo: req.userInfo, profileInfo: req.profileInfo }); res.json({ status: "ok" }); }); // TODO: Not sure what is this for, and why it doesn't check for auth app.post('/subscribe', (req, res) => { const subscription = req.body; res.status(201).json({}); const profileid = getProfileId(req); DB.setWebSubscription(profileid, subscription); }); // Function to Singup new users. An user is a combination of a user obj and a profile. // When new users are subscribed, they have a single profile, which is the personal one. // Other profiles can be link to that user, like groups or courses. const signup = async function (req, res) { const username = req.query.username || req.body.username; const password = req.query.password || req.body.password; const email = req.query.email || req.body.email; const profile = req.query.profile || req.body.profile; if (!username || !password || !email) return res.json({ status: "fail" }); // Check if the new user has an invitation. // TODO: Alert admin of signup attempts via email. if (!await DB.getInvitation(email)) return res.json({ status: "Not invitation found!" }); // Hash password to be stored on the DB. // TODO: I think this is missing a Salt factor to improve security const hashedPassword = await bcrypt.hash(password, 10); const newUserObject = await DB.newUser({ username: username.toLowerCase(), email: email.toLowerCase(), password: hashedPassword }); // If newUserObject it's an error message, we check by looking toLowerCase function if (!newUserObject.toLowerCase) { let user = { userid: toLowerCase.insertedId, profile: profile, } // Filter the provided information by the template, and adding to the DB, and login. const userObj = new Profile(user); await DB.newProfile(userObj); // TODO: this might fail, add catch scenarios return await login(req, res); } // Error return return res.json({ status: response }) } app.route('/signup').get(async (req, res) => { // Allow get and post to test on browser. return await signup(req, res); }).post(async (req, res) => { return await signup(req, res); }); const cookiesOptions = { maxAge: 1000 * 60 * 60 * 24 * 90, // would expire after 30 days httpOnly: true, // The cookie only accessible by the web server //signed: true // Indicates if the cookie should be signed sameSite: 'none', // This and secure are required for properly secure: true, // manage cockies in cros-domain }; // Function for user Login, it returns the coockies/json for user_id session_id and profile_id const login = async function (req, res) { // Check if user is already logged in and redirect to root if so. const session_id = getSessionId(req); const user_sid = getUserId(req); if (session_id && user_sid) { const userInfo = await DB.checkSessionOnDB(session_id, user_sid); if (userInfo) return res.redirect('/'); } const username = req.body.username || req.query.username; const password = req.body.password || req.query.password || ""; const user = await DB.getUser(username); if (!user) return res.json({ status: "user not founded" }); // TODO: Also add salt parameter here. const isSamePassword = await bcrypt.compare(password, user.password); if (!isSamePassword) return res.json({ status: "incorrect password" }); // Store a new session loging on DB, and use ID as session ID const sessionObj = await DB.newSession(user._id); // Create coockies with information for Auth res.cookie('user_sid', user._id, cookiesOptions); res.cookie('session_id', sessionObj.insertedId, cookiesOptions); // Chooses the most recent update profile as current active profile const latestUpdatedProfile = await DB.latestProfile(user._id); res.cookie('profile_id', latestProfile._id, cookiesOptions); return res.json({ status: "ok", user_sid: user._id, session_id: sessionObj.insertedId, profile_id: latestUpdatedProfile._id }); } app.route('/login').get(async (req, res) => { return await login(req, res); }).post(async (req, res) => { return await login(req, res); }); // Util function for generating new random password for users. // TODO: Probably we can do someting better here. function generatePassword() { var length = 8, charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", retVal = ""; for (var i = 0, n = charset.length; i < length; ++i) { retVal += charset.charAt(Math.floor(Math.random() * n)); } return retVal; } // Reset password will generate a new password and sent it as email to users app.route('/resetPassword').post(async (req, res) => { const session_id = getSessionId(req); const user_sid = getUserId(req); if (session_id && user_sid) { // Sadly reusing this endpoint to change password to legged in users. // TODO: Move change password logic to its own endpoint. const userInfo = await DB.checkSessionOnDB(session_id, user_sid); if (userInfo) { const password = req.body.password; const hashedPassword = await bcrypt.hash(password, 10); // TODO: Add salt to password here as well. DB.resetUserPassword(userInfo.username, hashedPassword); return res.json({ status: "ok", details: 'password changed!' // This should be an enum that syncs with clients. }); } } // Logic for non-logged in users. const username = req.body.username; const user = await DB.getUser(username); if (!user) return res.json({ status: "user not founded" }); const password = generatePassword(); const hashedPassword = await bcrypt.hash(password, 10); // TODO: Add salt to password here as well. // TODO: We need to limit this to every 2 hours or something like this. // TODO: Move this template to the Notif file. DB.resetUserPassword(username, hashedPassword); Notifications.sendEmail(username, "Your new credentials", `
Hello,
This is your new password: ${password}
Blessings
Emmanuel International Ministries
`) return res.json({ status: "ok", details: 'Check your email for new password' // Enum of details? }); }); // Change the active profile for the user. app.post('/changeProfile', sessionChecker, async (req, res) => { const user_sid = getUserId(req); let profile = await DB.getProfile(req.body.profileid); if (!profile) { return res.json({ status: "profile does not exists", }); } let isUserOwnerOfTheProfile = profile.userid != user_sid; if (isUserOwnerOfTheProfile) { return res.json({ status: "profile does not belong to the logged user", }); } // Change the coockie for active profile, this will inform all future requests res.cookie('profile_id', profile._id, cookiesOptions); return res.json({ status: "ok", ...profile }); }); // TODO: Not sure when is this used. Maybe for Notifs? app.post('/token/', sessionChecker, async (req, res) => { const profileid = getProfileId(req); let token = req.body.token DB.setProfileToken(profileid, token); return res.json({ status: "ok" }); }); // route for user logout const logout = function (req, res) { const session_id = getSessionId(req); const user_sid = getUserId(req); if (session_id && user_sid) { res.clearCookie('session_id'); res.clearCookie('user_sid'); //remove from DB DB.removeSession(session_id); res.redirect('/'); } else { res.redirect('/login'); } } app.get('/logout', (req, res) => { return logout(req, res); }); //Private Routes app.use('/user', sessionChecker, profileRoute); app.use('/post', sessionChecker, postRoute); app.use('/payments', sessionChecker, paymentsRoute); app.use('/bible', sessionChecker, bibleRoute); app.use('/songs', sessionChecker, songsRoute); //Public Routes app.use('/subsplash', subsplashRoute); // route for handling 404 requests(unavailable routes) app.use(function (req, res, next) { res.status(404).send("Sorry can't find that!") }); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`); }); }).catch((err) => { throw err; });