const MongoDB = require("../mongoDB.js"); const { client_logger } = require('../utils/analyticsLogger'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils.js'); const { getCookiesOptions } = require('../config/cookiesOptions'); const Notifications = require("../notifications"); // Object Definitions const Post = require("../def/post.js") const Profile = require("../def/profile.js"); const DUMMY_BCRYPT_HASH = '$2b$10$2zQfAaxK0cN13N7V2Q5hAOL3wxY5E9OQj1YxDCEV4VpWw2X2gYd6C'; const PASSWORD_TOKEN_TTL_MINUTES = parseInt(process.env.PASSWORD_TOKEN_TTL_MINUTES || '20', 10); const PASSWORD_TOKEN_PATH = process.env.PASSWORD_TOKEN_PATH || '/token-login'; const FRONTEND_URL = (process.env.FRONTEND_URL || 'https://social.emmint.com').replace(/\/+$/, ''); const createPasswordTokenHash = (rawToken) => crypto.createHash('sha256').update(rawToken).digest('hex'); const createSessionFromUser = async ({ DB, user, req, res }) => { const sessionObj = await DB.newSession(user._id); const cookiesOptions = getCookiesOptions(req); res.cookie('user_sid', user._id, cookiesOptions); res.cookie('session_id', sessionObj.insertedId, cookiesOptions); const latestUpdatedProfile = await DB.latestProfile(user._id); if (latestUpdatedProfile && latestUpdatedProfile._id) { res.cookie('profile_id', latestUpdatedProfile._id, cookiesOptions); } client_logger.identify({ distinctId: user._id, properties: { name: latestUpdatedProfile?.profile?.firstName || '', } }); client_logger.capture({ distinctId: user._id, event: 'server@' + req.method + '@' + req.originalUrl, }); return { status: "ok", user_sid: user._id, session_id: sessionObj.insertedId, profile_id: latestUpdatedProfile?._id }; }; // 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.body.username || "").trim().toLowerCase(); const password = req.body.password; const email = (req.body.email || "").trim().toLowerCase(); const profile = req.body.profile; if (!username || !password || !email) return res.json({ status: "Incomplete information!" }); // Check if the new user has an invitation. const DB = await MongoDB.getDB; if (!await DB.getInvitation(email)) { client_logger.capture({ distinctId: 'app_level', event: 'server@' + req.method + '@' + req.originalUrl + '@NotInvited', properties: { username: username, email: email, } }); return res.json({ status: "Not invitation found!" }); } let isUserAlreadyRegistered = await DB.getUser(email); if (isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({ status: "This user is already registered" }); const hashedPassword = await bcrypt.hash(password, 10); const newUserObject = await DB.newUser({ username, email, password: hashedPassword }); // If newUserObject it's an error message, we check by looking toLowerCase function if (!newUserObject.toLowerCase) { let user = { userid: newUserObject.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); client_logger.capture({ distinctId: newUserObject.insertedId, event: 'server@' + req.method + '@' + req.originalUrl, }); // TODO: this might fail, add catch scenarios return await login(req, res); } // Error return client_logger.capture({ distinctId: 'app_level', event: 'server@' + req.method + '@' + req.originalUrl + '@error', properties: { ustatus: newUserObject, } }); return res.json({ status: newUserObject }) } // 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); const DB = await MongoDB.getDB; if (session_id && user_sid) { const userInfo = await DB.checkSessionOnDB(session_id, user_sid); if (userInfo) return res.redirect('/'); } const invalidCredentials = () => res.status(401).json({ status: "Invalid credentials" }); const username = (req.body.username || req.body.email || "").trim().toLowerCase(); const password = req.body.password || ""; if (!username || !password) return invalidCredentials(); const user = await DB.getUser(username); if (!user) { client_logger.capture({ distinctId: 'app_level', event: 'server@' + req.method + '@' + req.originalUrl + '@invalidCredentials', properties: { username }, }); } const isSamePassword = await bcrypt.compare(password, user?.password || DUMMY_BCRYPT_HASH); if (!user || !isSamePassword) return invalidCredentials(); try { return res.json(await createSessionFromUser({ DB, user, req, res })); } catch (error) { console.error(error); client_logger.capture({ distinctId: 'app_level', event: 'server@' + req.method + '@' + req.originalUrl + '@error', }); return res.json({ status: "Error on this User Profile, please contact admin." }); } } // route for user logout const logout = async function (req, res) { const session_id = getSessionId(req); const user_sid = getUserId(req); if (session_id && user_sid) { const cookiesOptions = getCookiesOptions(req); res.clearCookie('session_id', cookiesOptions); res.clearCookie('user_sid', cookiesOptions); //remove from DB const DB = await MongoDB.getDB; DB.removeSession(session_id); client_logger.capture({ distinctId: user_sid, event: 'server@' + req.method + '@' + req.originalUrl, }); res.redirect('/'); } else { res.redirect('/login'); } } const resetPassword = async function (req, res) { const DB = await MongoDB.getDB; const genericResetResponse = { status: "ok", details: "If the account exists, check your email for next steps" }; // Logic for non-logged in users. const username = (req.body.username || req.body.email || "").trim().toLowerCase(); if (!username) return res.json(genericResetResponse); const user = await DB.getUser(username); if (!user) { client_logger.capture({ distinctId: 'app_level', event: 'server@' + req.method + '@' + req.originalUrl + '@resetRequestedUnknownUser', properties: { username } }); return res.json(genericResetResponse); } const rawToken = crypto.randomBytes(32).toString('hex'); const tokenHash = createPasswordTokenHash(rawToken); const expiresAt = new Date(Date.now() + PASSWORD_TOKEN_TTL_MINUTES * 60 * 1000); const tokenStored = await DB.createPasswordLoginToken(user._id, tokenHash, expiresAt); if (!tokenStored) { return res.json(genericResetResponse); } const loginUrl = `${FRONTEND_URL}${PASSWORD_TOKEN_PATH}?token=${rawToken}`; Notifications.sendEmail(username, "Your secure sign-in link", `

Hello,

Use this one-time sign-in link to access your account:

${loginUrl}

This link expires in ${PASSWORD_TOKEN_TTL_MINUTES} minutes and can only be used once.

If you did not request this, you can ignore this email.

Blessings

Emmanuel International Ministries

`); client_logger.capture({ distinctId: user._id, event: 'server@' + req.method + '@' + req.originalUrl, properties: { username: username, } }); return res.json(genericResetResponse); } const loginWithPasswordToken = async function (req, res) { const DB = await MongoDB.getDB; const token = (req.body.token || "").trim(); if (!token || token.length < 32) { return res.status(401).json({ status: "Invalid or expired token" }); } const tokenHash = createPasswordTokenHash(token); const tokenDoc = await DB.consumePasswordLoginToken(tokenHash); if (!tokenDoc || !tokenDoc.userId) { return res.status(401).json({ status: "Invalid or expired token" }); } const user = await DB.getUserById(tokenDoc.userId); if (!user || !user._id) { return res.status(401).json({ status: "Invalid or expired token" }); } try { return res.json(await createSessionFromUser({ DB, user, req, res })); } catch (error) { console.error("Token login error", error); return res.status(500).json({ status: "Internal server error" }); } } module.exports = { signup, login, logout, resetPassword, loginWithPasswordToken, }