// 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'); /** * @swagger * /signup: * post: * summary: Signs up a new user * tags: [Auth] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * email: * type: string * format: email * password: * type: string * format: password * responses: * 200: * description: The user was successfully signed up. * content: * application/json: * schema: * type: object * properties: * status: * type: string * 400: * description: Bad request. */ app.route('/signup').get(signup).post(signup); /** * @swagger * /login: * post: * summary: Logs in a user * tags: [Auth] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * email: * type: string * format: email * password: * type: string * format: password * responses: * 200: * description: The user was successfully logged in. * content: * application/json: * schema: * type: object * properties: * status: * type: string * 401: * description: Invalid credentials. */ app.route('/login').get(login).post(login); /** * @swagger * /logout: * get: * summary: Logs out a user * tags: [Auth] * responses: * 200: * description: The user was successfully logged out. * content: * application/json: * schema: * type: object * properties: * status: * type: string */ app.get('/logout', logout); /** * @swagger * /resetPassword: * post: * summary: Resets a user's password * tags: [Auth] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * email: * type: string * format: email * responses: * 200: * description: A password reset link has been sent to the user's email. * content: * application/json: * schema: * type: object * properties: * status: * type: string * 400: * description: Bad request. */ 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); // Swagger API Docs const swaggerJSDoc = require('swagger-jsdoc'); const swaggerUi = require('swagger-ui-express'); const swaggerDefinition = { openapi: '3.0.0', info: { title: 'EMI Backend API', version: '1.0.0', description: 'This is the REST API for the EMI Backend' }, servers: [ { url: 'http://localhost:3000', description: 'Development server' } ], components: { securitySchemes: { cookieAuth: { type: 'apiKey', in: 'cookie', name: 'user_sid' } } } }; const options = { swaggerDefinition, apis: ['./index.js', './routes/*.js'] }; const swaggerSpec = swaggerJSDoc(options); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // 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!"); /** * @swagger * /: * get: * summary: Returns basic information about the logged-in user * tags: [General] * security: * - cookieAuth: [] * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * userInfo: * type: object * profileInfo: * type: object * 401: * description: Unauthorized */ 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" }); } }); /** * @swagger * /invite/{email}: * get: * summary: Checks if an invitation exists for a given email * tags: [General] * parameters: * - in: path * name: email * required: true * schema: * type: string * format: email * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * invitation: * type: object * 400: * description: Provide a valid email * 404: * description: No invitation found for this email * 409: * description: This user is already registered */ 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" }); } }); /** * @swagger * /changeProfile: * post: * summary: Changes the active profile for the user * tags: [General] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * profileid: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * profile: * type: object * 400: * description: Profile ID is required * 403: * description: Profile does not belong to the logged-in user * 404: * description: Profile does not exist */ 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" }); } }); /** * @swagger * /token: * post: * summary: Refreshes the push notification token * tags: [General] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * token: * type: string * responses: * 200: * description: OK * content: * application/json: * schema: * type: object * properties: * status: * type: string * 400: * description: Token is required * 500: * description: Failed to update 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' }); } }); /** * @swagger * /subscribe: * post: * summary: Subscribes a user to webpush notifications * tags: [General] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * responses: * 201: * description: Created */ 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 };