// 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(); // Server Application using express const express = require('express'); const app = express(); const port = process.env.PORT || 3000; // -- Accept request from other origins const cors = require('cors'); const { corsOptions } = require('./config/corsOptions'); app.use(cors(corsOptions)); // -- Parse incoming requests with JSON payloads const bodyParser = require('body-parser'); app.use(bodyParser.json()); // -- Parse incoming requests with urlencoded payloads app.use(bodyParser.urlencoded({ extended: true })); // -- Parse cookies const cookieParser = require('cookie-parser'); app.use(cookieParser()); // -- Rate limiting const { rateLimit } = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 10 * 60 * 1000, // 15 minutes limit: 500, // Limit each IP to 100 requests per `window` (here, per 15 minutes). standardHeaders: 'draft-8', // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header legacyHeaders: false, // Disable the `X-RateLimit-*` headers. keyGenerator: (req) => { const forwarded = req.headers["x-forwarded-for"]?.split(",")[0]; // Take the first IP in the list const ip = forwarded || req.ip; // Fallback to req.ip return ip.includes(":") ? ip.split(":")[0] : ip; // Remove port if present } }); app.set('trust proxy', true); app.use(limiter); // Authentication const { signup, login, logout, resetPassword } = require('./auth/authEmail.js'); app.route('/signup').get(signup).post(signup); app.route('/login').get(login).post(login); app.get('/logout', logout); app.route('/resetPassword').post(resetPassword); // 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 bibleRoute = require('./routes/bible.js'); const sessionChecker = require('./middleware/sessionChecker'); // -- Private Routes app.use('/user', sessionChecker, profileRoute); app.use('/post', sessionChecker, postRoute); app.use('/payments', paymentsRoute); app.use('/bible', sessionChecker, bibleRoute); app.use('/songs', sessionChecker, songsRoute); // -- Public Routes const subsplashRoute = require('./routes/subsplash.js'); app.use('/subsplash', subsplashRoute); // Web Push Notifications const webPush = require('web-push'); 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); const { cookiesOptions } = require('./config/cookiesOptions'); const { client_logger } = require('./utils/analyticsLogger.js'); const { getSessionId, getUserId, getProfileId } = require('./utils/sessionUtils.js'); // Only bind the port if the DB connection is successful const DB = require("./mongoDB.js"); DB.getDB.then((DB) => { console.log("Main logic: DB connected!"); // route for Home-Page app.get('/', sessionChecker, async (req, res) => { try { const userInfo = req.userInfo; if (!userInfo) { // This should not happend, since the sessionChecker should redirect to login return res.status(401).json({ status: "Unauthorized" }); } return res.json({ status: "ok", userInfo: req.userInfo, profileInfo: req.profileInfo }); } catch (error) { console.error("Error processing / path", error); return res.status(500).json({ status: "Internal server error" }); } }); // Check for an invitation for an email app.get("/invite/:email", async (req, res) => { try { const email = req.params.email.trim().toLowerCase(); // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ status: "Provide a valid email" }); } // Check for an invitation const invitation = await DB.getInvitation(email); if (!invitation) { return res.status(404).json({ status: "No invitation found for this email" }); } // Check if the user is already registered const existingUser = await DB.getUser(email); if (existingUser?._id) { return res.status(409).json({ status: "This user is already registered" }); } // Capture event in logging client_logger.capture({ distinctId: 'app_level', event: `server@${req.method}@${req.originalUrl}`, }); // Respond with invitation details return res.json({ status: "ok", invitation }); } catch (error) { console.error("Error processing invite check:", error); return res.status(500).json({ status: "Internal server error" }); } }); // Change the active profile for the user. app.post('/changeProfile', sessionChecker, async (req, res) => { try { const user_sid = getUserId(req); const profileId = req.body.profileid; // Validate input if (!profileId) { return res.status(400).json({ status: "Profile ID is required" }); } // Fetch profile from DB const profile = await DB.getProfile(profileId); if (!profile) { return res.status(404).json({ status: "Profile does not exist" }); } // Check ownership if (profile.userid !== user_sid) { return res.status(403).json({ status: "Profile does not belong to the logged-in user" }); } // Update active profile cookie res.cookie('profile_id', profile._id, cookiesOptions); return res.json({ status: "ok", profile }); } catch (error) { console.error("Error changing profile:", error); return res.status(500).json({ status: "Internal server error" }); } }); // This is the endpoint to refresh the push notification token app.post('/token/', sessionChecker, async (req, res) => { try { const profileid = getProfileId(req); const { token } = req.body; // Validate token presence if (!token) { return res.status(400).json({ status: 'Token is required' }); } // Set the token in the database and wait for completion const result = await DB.setProfileToken(profileid, token); // Assuming DB.setProfileToken() could fail, handle it appropriately if (!result) { return res.status(500).json({ status: 'Failed to update token' }); } return res.json({ status: 'ok' }); } catch (error) { console.error('Error setting profile token:', error); return res.status(500).json({ status: 'Internal server error' }); } }); // Used for webpush notifications app.post('/subscribe', sessionChecker, async (req, res) => { const subscription = req.body; res.status(201).json({}); const profileid = getProfileId(req); if (profileid) await DB.setWebSubscription(profileid, subscription); }); // 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(`Fellowship app running at http://localhost:${port}`); }).on('error', (err) => { console.error('Server failed to start:', err); }); }).catch((err) => { console.error('Error connecting to MongoDB:', err); throw err; }); // Export the app for testing purposes module.exports = { app, mongoDB: DB };