17 Commits

Author SHA1 Message Date
Adolfo Reyna
f3a782a360 chore(auth): remove security plan doc and marker comments 2026-02-20 21:22:47 -05:00
Adolfo Reyna
19d805d322 fix(auth): add account+IP brute-force rate limiting 2026-02-20 21:20:21 -05:00
Adolfo Reyna
469962d03c fix(auth): add single-use token login recovery flow 2026-02-20 20:20:40 -05:00
Adolfo Reyna
c6d9dfd3c1 fix(auth): enforce POST body credentials and generic auth errors 2026-02-20 20:09:29 -05:00
Adolfo Reyna
0baf237548 docs(auth): add password security hardening plan and code markers 2026-02-20 20:07:26 -05:00
Adolfo Reyna
ea864b27d4 Harden local/prod cookie policy and Mongo connection settings 2026-02-20 19:25:21 -05:00
Adolfo Reyna
c136d25974 Harden feed/profile routes against invalid IDs and null profiles 2026-02-20 19:09:15 -05:00
Adolfo Reyna
0b36db9b33 docs: Update Swagger documentation for Profile endpoints
Updated the Swagger documentation for various Profile endpoints to accurately
reflect their return types, including arrays of Profile objects and detailed
schemas for specific responses.
2025-07-17 10:32:22 -04:00
Adolfo Reyna
e4bac717f9 docs: Add API documentation access instructions to README
Updated the README.md file to include instructions on how to access the
interactive API documentation via Swagger UI, including the URL.
2025-07-17 10:00:31 -04:00
Adolfo Reyna
d21736d52c docs: Update Swagger documentation for Post endpoints
Updated the Swagger documentation for various Post endpoints to accurately
reflect their return types, including arrays of Post objects and detailed
schemas for specific responses.
2025-07-17 09:59:50 -04:00
Adolfo Reyna
148ed696b2 feat: Add Swagger API documentation
This commit introduces Swagger API documentation for all endpoints in the
application.

- Installs  and .
- Configures Swagger in  to generate and serve API documentation
  at .
- Adds JSDoc-style Swagger annotations to all routes in  and
  the  directory (, , ,
  , , ).
- Defines a cookie-based security scheme for authenticated routes.

This allows for interactive API documentation and testing via the
endpoint.
2025-07-17 09:52:37 -04:00
Adolfo Reyna
0a48327e93 Improve readme (AI) 2025-07-17 09:37:21 -04:00
Adolfo Reyna
100b0c2a8f Change payments to be accessible wihtout loggin 2025-07-17 09:32:49 -04:00
Adolfo Reyna
64ca9df639 Tags support 2025-07-17 09:32:27 -04:00
Adolfo Reyna
46a2fc5c2b Add endpoint to retrieve posts by tag with validation 2025-02-27 23:45:17 -05:00
Adolfo Reyna
56cb8b4caa Enhance session and profile handling with validation and error handling improvements 2025-02-27 23:12:11 -05:00
Adolfo Reyna
606db78529 Add data to push notifications 2025-02-27 23:11:53 -05:00
21 changed files with 3138 additions and 348 deletions

139
README.md
View File

@@ -2,8 +2,141 @@
This is the code for the backend of the EMI website.
## Getting Started
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
### Prerequisites
What things you need to install the software and how to install them:
```
node.js
npm
mongodb
```
### Installing
A step by step series of examples that tell you how to get a development env running:
1. Clone the repo
2. Install NPM packages
```
npm install
```
3. Create a `.env` file with the necessary environment variables (`PORT`, `MONGO_URL`, `STRIPE`, etc.)
4. Run the server
```
npm start
```
### API Documentation
Once the server is running, you can access the interactive API documentation powered by Swagger UI at:
`http://localhost:3000/api-docs`
This page allows you to view all available endpoints, their parameters, and test them directly from your browser.
## API Endpoints
The API is divided into several sections based on functionality. Most routes under `/user`, `/post`, `/bible`, and `/songs` require authentication via a session cookie.
### Authentication
- `POST /signup`: Creates a new user account.
- `POST /login`: Logs in a user and creates a session.
- `GET /logout`: Logs out the current user.
- `POST /resetPassword`: Sends a password reset link to the user's email.
### General
- `GET /`: Returns basic information about the logged-in user.
- `GET /invite/:email`: Checks if an invitation exists for a given email.
- `POST /changeProfile`: Changes the active profile for the logged-in user.
- `POST /token`: Refreshes the push notification token for a profile.
- `POST /subscribe`: Subscribes a profile to web push notifications.
### Profiles (`/user`)
- `GET /mine`: Get all profiles for the logged-in user.
- `POST /`: Creates a new profile.
- `POST /invite`: Invites a new user by email.
- `GET /invite/:email`: Get invitation details for an email.
- `GET /groups`: Get a list of all groups.
- `GET /groups/following`: Get a list of groups the current profile is following.
- `POST /groups`: Create a new group.
- `GET /courses`: Get a list of all courses.
- `POST /groups/accept`: Accept a request to join a private group.
- `POST /groups/reject`: Reject a request to join a private group.
- `GET /groups/search`: Search for groups.
- `GET /groups/:id`: Get details for a specific group.
- `GET /groups/:id/subscribe`: Subscribe to a group.
- `GET /groups/:id/unsubscribe`: Unsubscribe from a group.
- `GET /search`: Search for profiles.
- `POST /setData`: Set custom data for a profile.
- `POST /myProfile`: Update the current user's profile.
- `GET /:id`: Get a specific profile by ID.
- `DELETE /:id`: Delete a profile.
- `GET /:id/follow`: Follow a profile.
- `GET /:id/unfollow`: Unfollow a profile.
### Posts (`/post`)
- `GET /organic`: Get the organic feed for the current user.
- `GET /`: Get the feed with promotional content.
- `GET /tag/:tag`: Get posts with a specific tag.
- `GET /usr/:id`: Get posts from a specific user.
- `GET /usr/:id/images`: Get all image posts from a user.
- `GET /usr/:id/embedded`: Get all embedded posts from a user.
- `GET /usr/:id/media`: Get all media posts from a user.
- `POST /`: Create a new post.
- `POST /react`: React to a post.
- `POST /unreact`: Remove a reaction from a post.
- `POST /bookmark`: Bookmark a post.
- `POST /unbookmark`: Remove a bookmark from a post.
- `POST /comment`: Add a comment to a post.
- `POST /comment/react`: React to a comment.
- `POST /comment/unreact`: Remove a reaction from a comment.
- `GET /images`: Get all image posts for the current user.
- `GET /embedded`: Get all embedded posts for the current user.
- `GET /media`: Get all media posts for the current user.
- `GET /course/recent`: Get recently watched media from courses.
- `GET /:id`: Get a specific post by ID.
- `DELETE /:id`: Delete a post.
- `POST /:id`: Update a post.
### Payments (`/payments`)
- `POST /create-payment-intent`: Creates a Stripe Payment Intent.
- `POST /intent`: (Alias for /create-payment-intent)
- `POST /register`: Registers a payment after a successful Stripe transaction.
### Songs (`/songs`)
- `GET /`: Get all songs.
- `POST /`: Create a new song.
- `GET /:id`: Get a specific song by ID.
- `DELETE /:id`: Delete a song.
- `POST /:id`: Update a song.
### Bible (`/bible`)
- `GET /`: Get a list of available Bibles.
- `GET /books`: Get the books of a Bible.
- `GET /books/:bookId`: Get details for a specific book.
- `GET /books/:bookId/chapters`: Get the chapters of a book.
- `GET /chapters/:chapterId`: Get the content of a chapter.
- `GET /chapters/:chapterId/verses`: Get the verses of a chapter.
- `GET /search`: Search the Bible.
### Subsplash (`/subsplash`)
- `GET /events/:calendarId`: Get events from a Subsplash calendar.
- `GET /media/:seriesId`: Get media from a Subsplash media series.
### TODO
[ ] Define nodes schema
[ ] Implement basic login/registration
[ ]
- [ ] Define nodes schema
- [ ] Implement basic login/registration

View File

@@ -9,15 +9,48 @@ 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);
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.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;
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;
@@ -34,12 +67,10 @@ const signup = async function (req, res) {
}
let isUserAlreadyRegistered = await DB.getUser(email);
if (isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({ status: "This user is already registered" });
// 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(),
username,
email,
password: hashedPassword
});
// If newUserObject it's an error message, we check by looking toLowerCase function
@@ -74,52 +105,28 @@ 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 username = req.body.username || req.query.username;
const password = req.body.password || req.query.password || "";
const DB = await MongoDB.getDB;
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 + '@userNotFound',
properties: {
username: username,
}
event: 'server@' + req.method + '@' + req.originalUrl + '@invalidCredentials',
properties: { username },
});
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" });
const isSamePassword = await bcrypt.compare(password, user?.password || DUMMY_BCRYPT_HASH);
if (!user || !isSamePassword) return invalidCredentials();
try {
// 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', 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 res.json({
status: "ok",
user_sid: user._id,
session_id: sessionObj.insertedId,
profile_id: latestUpdatedProfile._id
});
return res.json(await createSessionFromUser({ DB, user, req, res }));
} catch (error) {
console.error(error);
client_logger.capture({
@@ -151,51 +158,43 @@ const logout = async function (req, res) {
}
}
// Util function for generating new random password for users.
function generatePassword(length = 12) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
return Array.from(crypto.randomFillSync(new Uint8Array(length)))
.map((x) => charset[x % charset.length])
.join("");
}
const resetPassword = async function (req, res) {
const session_id = getSessionId(req);
const user_sid = getUserId(req);
const DB = await MongoDB.getDB;
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({
const genericResetResponse = {
status: "ok",
details: 'password changed!' // This should be an enum that syncs with clients.
});
}
}
details: "If the account exists, check your email for next steps"
};
// Logic for non-logged in users.
const username = req.body.username;
const username = (req.body.username || req.body.email || "").trim().toLowerCase();
if (!username) return res.json(genericResetResponse);
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",
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",
`
<p> Hello,</p>
<p> This is your new password: ${password}</p>
<p><a href="https://social.emmint.com/">Log in</a></p>
<p>Hello,</p>
<p>Use this one-time sign-in link to access your account:</p>
<p><a href="${loginUrl}">${loginUrl}</a></p>
<p>This link expires in ${PASSWORD_TOKEN_TTL_MINUTES} minutes and can only be used once.</p>
<p>If you did not request this, you can ignore this email.</p>
<p>Blessings</p>
<p>Emmanuel International Ministries</p>
`)
`);
client_logger.capture({
distinctId: user._id,
event: 'server@' + req.method + '@' + req.originalUrl,
@@ -203,10 +202,30 @@ const resetPassword = async function (req, res) {
username: username,
}
});
return res.json({
status: "ok",
details: 'Check your email for new password' // Enum of details?
});
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" });
}
}
@@ -215,4 +234,5 @@ module.exports = {
login,
logout,
resetPassword,
loginWithPasswordToken,
}

View File

@@ -1,8 +1,12 @@
const isProduction = process.env.NODE_ENV === "production";
const forceSecureCookie = process.env.COOKIE_SECURE === "true";
const secure = forceSecureCookie || isProduction;
const cookiesOptions = {
maxAge: 1000 * 60 * 60 * 24 * 90, // would expire after 30 days
maxAge: 1000 * 60 * 60 * 24 * 90, // would expire after 90 days
httpOnly: true, // The cookie only accessible by the web server
sameSite: 'none', // This and secure are required for properly
secure: true, // manage cockies in cros-domain
sameSite: secure ? 'none' : 'lax',
secure,
};
module.exports = { cookiesOptions };

View File

@@ -1,6 +1,10 @@
var corsOptions = {
origin: [
'http://localhost:8080',
'http://localhost:8081',
'http://127.0.0.1:3000',
'http://127.0.0.1:8080',
'http://127.0.0.1:8081',
'http://localhost:3000',
"https://social.emmint.com",
"https://fellowship.emmint.com",

View File

@@ -233,7 +233,7 @@ postDB = (DB)=>{
if(!DB.ObjectID.isValid(profileId)) return [];
const profile = await DB.getProfile(profileId);
if(!profile) return [];
query = {
const query = {
nonOrganicType: null // Exlcude news
};
return DB.postCols.find(query).sort({lastUpdated: -1}).limit(50).toArray().then(async (posts)=>{
@@ -244,6 +244,25 @@ postDB = (DB)=>{
});
}
// For all post with tags const query = { content: { $regex: '#\\w+', $options: 'i' } };
DB.getPostsByTag = async (tag, profileId, limit = 50) => {
if(!DB.ObjectID.isValid(profileId)) return [];
const profile = await DB.getProfile(profileId);
if(!profile) return [];
let query = {
content: {
"$regex": tag
},
nonOrganicType: null // Exlcude news
};
return DB.postCols.find(query).sort({lastUpdated: -1}).limit(limit).toArray().then(async (posts)=>{
return await filterPrivateGroups(posts, profile);
}).catch((err)=>{
console.log(err);
return false;
});
}
DB.getNews = async () => {
let query = {
nonOrganicType: 'News'

View File

@@ -15,7 +15,7 @@ userDB = (DB) => {
DB.removeProfile = (profileid) => {
const _id = DB.ObjectID(profileid);
if (userProfileCache[profileid]) delete userProfileCache[profileid];
return DB.profileCols.deleteOne({_id}).catch((err)=>{
return DB.profileCols.deleteOne({ _id }).catch((err) => {
console.log(err);
return false;
});
@@ -23,7 +23,7 @@ userDB = (DB) => {
DB.updateProfile = async (profileid, profileObj) => {
let tempProfile = profileObj.toObj();
const query = {_id: profileid};
const query = { _id: profileid };
const update = {
$set: {
profile: tempProfile.profile,
@@ -39,8 +39,8 @@ userDB = (DB) => {
DB.getProfile = async (profileId) => {
//if (userProfileCache[profileId] && !userProfileCache[profileId].isGroup) return userProfileCache[profileId];
if(!profileId) return false;
try{
if (!profileId) return false;
try {
const _id = DB.ObjectID(profileId);
let r = await DB.profileCols.findOne({ _id }).catch((err) => {
console.log(err);
@@ -48,7 +48,7 @@ userDB = (DB) => {
});
if (r) userProfileCache[profileId] = r;
return r;
}catch(_){
} catch (_) {
return {};
}
}
@@ -56,16 +56,16 @@ userDB = (DB) => {
DB.getPopularProfiles = async (limit = 10) => {
return DB.profileCols.aggregate([
{
$match: {isGroup: {$ne: true}}
$match: { isGroup: { $ne: true } }
},
{
$addFields: { subscribed_count: {$size: { "$ifNull": [ "$following", [] ] } } }
$addFields: { subscribed_count: { $size: { "$ifNull": ["$following", []] } } }
},
{
$sort: {"subscribed_count":-1}
$sort: { "subscribed_count": -1 }
},
{
$project: {_id: 1, "subscribed_count": 1}
$project: { _id: 1, "subscribed_count": 1 }
}
]).limit(limit).toArray().catch((err) => {
console.log(err);
@@ -76,16 +76,16 @@ userDB = (DB) => {
DB.getPopularGroups = async (limit = 10) => {
return DB.profileCols.aggregate([
{
$match: {isGroup: true, isPrivate: {$ne: true}, isCourse: {$ne: true}}
$match: { isGroup: true, isPrivate: { $ne: true }, isCourse: { $ne: true } }
},
{
$addFields: { subscribed_count: {$size: { "$ifNull": [ {"$objectToArray" : "$subscribed"}, [] ] } } }
$addFields: { subscribed_count: { $size: { "$ifNull": [{ "$objectToArray": "$subscribed" }, []] } } }
},
{
$sort: {"subscribed_count":-1}
$sort: { "subscribed_count": -1 }
},
{
$project: {_id: 1, "subscribed_count": 1}
$project: { _id: 1, "subscribed_count": 1 }
}
]).limit(limit).toArray().catch((err) => {
console.log(err);
@@ -95,19 +95,21 @@ userDB = (DB) => {
DB.getFriendsFriends = async (profileId, limit = 10) => {
const profile = await DB.getProfile(profileId);
if(!profile) return [];
let ids = profile.following.map((id)=>DB.ObjectID(id));
if (!profile) return [];
const following = Array.isArray(profile.following) ? profile.following : [];
let ids = following.filter((id) => DB.ObjectID.isValid(id)).map((id) => DB.ObjectID(id));
let alreadyFollowingMap = {};
alreadyFollowingMap[profileId] = 1; //skip that profile
profile.following.forEach(id => {
if(!alreadyFollowingMap[id]) alreadyFollowingMap[id] = 1;
following.forEach(id => {
if (!alreadyFollowingMap[id]) alreadyFollowingMap[id] = 1;
})
return DB.profileCols.find({_id:{$in: ids}}).project({following: 1}).limit(limit).toArray().then(profiles => {
return DB.profileCols.find({ _id: { $in: ids } }).project({ following: 1 }).limit(limit).toArray().then(profiles => {
let friendsOfFriendsMap = {};
profiles.forEach(p => {
p.following.forEach(followingId => {
if(alreadyFollowingMap[followingId]) return 0;
if(!friendsOfFriendsMap[followingId]) friendsOfFriendsMap[followingId] = 0;
const related = Array.isArray(p.following) ? p.following : [];
related.forEach(followingId => {
if (alreadyFollowingMap[followingId]) return 0;
if (!friendsOfFriendsMap[followingId]) friendsOfFriendsMap[followingId] = 0;
friendsOfFriendsMap[followingId] = friendsOfFriendsMap[followingId] + 1;
});
});
@@ -124,24 +126,38 @@ userDB = (DB) => {
return DB.getProfile(profileId);
}
DB.getProfileCache = async (profileId) => {
const cachedProfile = userProfileCache[profileId];
if (cachedProfile?.isGroup === false) {
return cachedProfile;
}
return await DB.getProfile(profileId);
};
DB.searchProfile = async (queryStr) => {
let regEx = new RegExp(queryStr, 'i');
let query = {
isGroup: false,
isChat: {$ne: true},
isChat: { $ne: true },
$or: [
{"profile.firstName": {
{
"profile.firstName": {
$regex: regEx
}},
{"profile.lastName": {
}
},
{
"profile.lastName": {
$regex: regEx
}},
{"profile.description": {
}
},
{
"profile.description": {
$regex: regEx
}},
}
},
]
};
let r = await DB.profileCols.find(queryStr ? query : {isGroup: false, isChat: {$ne: true}})
let r = await DB.profileCols.find(queryStr ? query : { isGroup: false, isChat: { $ne: true } })
.sort({ lastUpdate: -1 }).limit(20)
.toArray().catch((err) => {
console.log(err);
@@ -154,7 +170,7 @@ userDB = (DB) => {
const userid = DB.ObjectID(userId);
return await DB.profileCols.find({ userid }).toArray().catch((err) => {
console.log(err);
return false;
return [];
});
}
@@ -167,32 +183,32 @@ userDB = (DB) => {
return false;
});
let index = 0;
while(r[index].isGroup || r[index].isChat) index += 1;
while (r[index].isGroup || r[index].isChat) index += 1;
if (r[index]) userProfileCache[r[index]._id] = r[index];
return r[index];
}
DB.followProfile = async (profileId, followProfileId)=>{
DB.followProfile = async (profileId, followProfileId) => {
const _id = DB.ObjectID(profileId);
let update = {
$addToSet:{
$addToSet: {
following: followProfileId + '' //converts to str
}
}
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
}
DB.unfollowProfile = async (profileId, followProfileId)=>{
DB.unfollowProfile = async (profileId, followProfileId) => {
const _id = DB.ObjectID(profileId);
let update = {
$pull:{
$pull: {
following: followProfileId + '' //converts to str
}
}
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -200,7 +216,7 @@ userDB = (DB) => {
DB.getFollowingTheProfile = async (profileId) => {
//const profile_id = DB.ObjectID(profileId);
let r = await DB.profileCols.find({ following: (profileId+'') })
let r = await DB.profileCols.find({ following: (profileId + '') })
.toArray().catch((err) => {
console.log(err);
return [];
@@ -216,40 +232,40 @@ userDB = (DB) => {
DB.setData = async (profileid, key, value) => {
const _id = DB.ObjectID(profileid);
let update = {
$set:{
$set: {
["data." + key]: value
}
}
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
}
DB.setProfileToken = (profileid, token)=>{
if(!token) return false;
DB.setProfileToken = (profileid, token) => {
if (!token) return false;
const _id = DB.ObjectID(profileid);
let update = {
$addToSet:{
$addToSet: {
token
}
}
if (userProfileCache[profileid]) delete userProfileCache[profileid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
}
DB.setWebSubscription = (profileid, webSubscription)=>{
DB.setWebSubscription = (profileid, webSubscription) => {
const _id = DB.ObjectID(profileid);
let update = {
$set:{
$set: {
webSubscription
}
}
if (userProfileCache[profileid]) delete userProfileCache[profileid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -258,7 +274,7 @@ userDB = (DB) => {
DB.addNotification = async (profileid, message, postid, commentIndx, actorid) => {
const _id = DB.ObjectID(profileid);
let update = {
$push:{
$push: {
notifications: {
ts: new Date(),
body: message,
@@ -268,7 +284,7 @@ userDB = (DB) => {
}
}
}
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -283,10 +299,10 @@ userDB = (DB) => {
DB.getGroups = async (excludePrivate = false) => {
let query = {
isGroup: true,
isCourse: {$ne: true},
isChat: {$ne: true},
isCourse: { $ne: true },
isChat: { $ne: true },
};
if(excludePrivate) query.isPrivate = false;
if (excludePrivate) query.isPrivate = false;
let r = await DB.profileCols.find(query).sort({ lastUpdate: -1 }).limit(10)
.toArray().catch((err) => {
console.log(err);
@@ -298,21 +314,22 @@ userDB = (DB) => {
DB.getFollowingGroups = async (profileid) => {
const profile = await DB.getProfile(profileid);
let ids = [];
for(id in profile.following){
try{
let oId = DB.ObjectID(profile.following[id]);
const following = Array.isArray(profile?.following) ? profile.following : [];
for (id in following) {
try {
let oId = DB.ObjectID(following[id]);
let checkProfile = await DB.getProfileCache(oId)
if(checkProfile && checkProfile.isGroup && !checkProfile.isChat){
if (checkProfile && checkProfile.isGroup && !checkProfile.isChat) {
ids.push(oId)
}
}catch{
} catch {
}
}
let query = {
isGroup: true,
isCourse: {$ne: true},
isChat: {$ne: true},
isCourse: { $ne: true },
isChat: { $ne: true },
_id: {
$in: ids
}
@@ -329,23 +346,31 @@ userDB = (DB) => {
let regEx = new RegExp(queryStr, 'i');
let query = queryStr ? {
isGroup: true,
isChat: {$ne: true},
isChat: { $ne: true },
isCourse: coursesB,
$or: [
{"profile.firstName": {
{
"profile.firstName": {
$regex: regEx
}},
{"profile.lastName": {
}
},
{
"profile.lastName": {
$regex: regEx
}},
{"profile.description": {
}
},
{
"profile.description": {
$regex: regEx
}},
{"data.author": {
}
},
{
"data.author": {
$regex: regEx
}}
}
}
]
} : {isGroup: true, isChat: {$ne: true}, isCourse: coursesB};
} : { isGroup: true, isChat: { $ne: true }, isCourse: coursesB };
let r = await DB.profileCols.find(query)
.sort({ lastUpdate: -1 }).limit(20)
.toArray().catch((err) => {
@@ -357,13 +382,13 @@ userDB = (DB) => {
let privateGroupsCache = {};
DB.isGroupPrivate = async (groupid) => {
if(userProfileCache[groupid]) return userProfileCache[groupid].isPrivate;
if (userProfileCache[groupid]) return userProfileCache[groupid].isPrivate;
let g = await DB.getGroup(groupid);
return g ? g.isPrivate : false;
}
DB.isGroupNewsOnly = async (groupid) => {
if(userProfileCache[groupid]) return userProfileCache[groupid].newsOnly;
if (userProfileCache[groupid]) return userProfileCache[groupid].newsOnly;
let g = await DB.getGroup(groupid);
return g ? g.newsOnly : false;
}
@@ -377,7 +402,7 @@ userDB = (DB) => {
DB.getGroup = async (groupid) => {
const _id = DB.ObjectID(groupid);
//if(userProfileCache[groupid]) return userProfileCache[groupid];
let r = await DB.profileCols.findOne({_id, isGroup: true, isChat: {$ne: true},}).catch((err) => {
let r = await DB.profileCols.findOne({ _id, isGroup: true, isChat: { $ne: true }, }).catch((err) => {
console.log(err);
return false;
});
@@ -389,13 +414,13 @@ userDB = (DB) => {
const _id = DB.ObjectID(groupid);
const subOrRequest = reqSubscription ? "pending." : "subscribed.";
let update = {
$set:{
$set: {
[subOrRequest + profileid]: new Date()
}
}
if(!reqSubscription) DB.followProfile(profileid, groupid);
if (!reqSubscription) DB.followProfile(profileid, groupid);
delete userProfileCache[groupid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -404,16 +429,16 @@ userDB = (DB) => {
DB.acceptGroupJoinReq = async (profileid, groupid) => {
const _id = DB.ObjectID(groupid);
let update = {
$set:{
$set: {
["subscribed." + profileid]: new Date()
},
$unset:{
$unset: {
["pending." + profileid]: ""
}
}
DB.followProfile(profileid, groupid);
delete userProfileCache[groupid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -422,12 +447,12 @@ userDB = (DB) => {
DB.rejectGroupJoinReq = async (profileid, groupid) => {
const _id = DB.ObjectID(groupid);
let update = {
$unset:{
$unset: {
["pending." + profileid]: ""
}
}
delete userProfileCache[groupid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -436,12 +461,12 @@ userDB = (DB) => {
DB.unsubscribeToGroup = async (profileid, groupid) => {
const _id = DB.ObjectID(groupid);
let update = {
$unset:{
$unset: {
["subscribed." + profileid]: "",
}
}
DB.unfollowProfile(profileid, groupid)
return DB.profileCols.updateOne({_id}, update).catch((err)=>{
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err);
return false;
});
@@ -449,7 +474,7 @@ userDB = (DB) => {
//Courses
DB.getCourses = async () => {
let r = await DB.profileCols.find({isGroup: true, isCourse: true, isChat: {$ne: true}})
let r = await DB.profileCols.find({ isGroup: true, isCourse: true, isChat: { $ne: true } })
.sort({ lastUpdate: -1 }).limit(20)
.toArray().catch((err) => {
console.log(err);

View File

@@ -1,15 +1,15 @@
class User {
constructor(info){
if(!info || !info.userid) throw "Can not construct empty profile";
if(!info || !info.userid) throw new Error("Cannot construct empty profile");
this.userid = info.userid;
this.profile = {
firstName: info.profile && info.profile.firstName || '',
lastName: info.profile && info.profile.lastName || '',
photo: info.profile && info.profile.photo || '',
location: info.profile && info.profile.location || 'USA',
language: info.profile && info.profile.language || 'en',
status: info.profile && info.profile.status || '',
description: info.profile && info.profile.description || '',
firstName: info.profile?.firstName || '',
lastName: info.profile?.lastName || '',
photo: info.profile?.photo || '',
location: info.profile?.location || 'USA',
language: info.profile?.language || 'en',
status: info.profile?.status || '',
description: info.profile?.description || '',
};
this.data = info.data || {};
this.username = info.username || '';
@@ -23,8 +23,8 @@ class User {
this.isCourse = info.isCourse || false;
this.isPrivate = info.isPrivate || false;
this.isChat = info.isChat || false;
this.subscribed = info.subscribed || {}; //Subscribed user to groups
this.pending = info.pending || {}; //Private groups require authorization
this.subscribed = JSON.parse(JSON.stringify(info.subscribed || {})); //Subscribed user to groups
this.pending = JSON.parse(JSON.stringify(info.pending || {})); //Private groups require authorization
}
toObj(){

329
index.js
View File

@@ -9,6 +9,7 @@ require('dotenv').config();
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.set('trust proxy', true);
// -- Accept request from other origins
const cors = require('cors');
const { corsOptions } = require('./config/corsOptions');
@@ -34,15 +35,147 @@ const limiter = rateLimit({
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);
const { signup, login, logout, resetPassword, loginWithPasswordToken } = require('./auth/authEmail.js');
const { authRateLimiter } = require('./middleware/authRateLimiter');
/**
* @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.post('/signup', 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.post('/login', authRateLimiter('login'), 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);
app.route('/resetPassword').post(resetPassword);
/**
* @swagger
* /resetPassword:
* post:
* summary: Sends a one-time sign-in link if the account exists
* 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(authRateLimiter('reset'), resetPassword);
/**
* @swagger
* /password/token-login:
* post:
* summary: Consumes a one-time password token and starts a session
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* token:
* type: string
* responses:
* 200:
* description: Logged in with one-time token
* 401:
* description: Invalid or expired token
*/
app.post('/password/token-login', authRateLimiter('token'), loginWithPasswordToken);
// Routes
const profileRoute = require('./routes/profile.js');
@@ -54,13 +187,49 @@ const sessionChecker = require('./middleware/sessionChecker');
// -- Private Routes
app.use('/user', sessionChecker, profileRoute);
app.use('/post', sessionChecker, postRoute);
app.use('/payments', sessionChecker, paymentsRoute);
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;
@@ -78,7 +247,31 @@ const DB = require("./mongoDB.js");
DB.getDB.then((DB) => {
console.log("Main logic: DB connected!");
// route for Home-Page
/**
* @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;
@@ -97,7 +290,38 @@ DB.getDB.then((DB) => {
}
});
// Check for an invitation for an email
/**
* @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();
@@ -130,7 +354,42 @@ DB.getDB.then((DB) => {
});
// Change the active profile for the user.
/**
* @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);
@@ -157,7 +416,38 @@ DB.getDB.then((DB) => {
}
});
// This is the endpoint to refresh the push notification token
/**
* @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);
@@ -179,7 +469,24 @@ DB.getDB.then((DB) => {
}
});
// Used for webpush notifications
/**
* @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({});

View File

@@ -0,0 +1,116 @@
const crypto = require('crypto');
const { client_logger } = require('../utils/analyticsLogger');
const AUTH_ATTEMPT_WINDOW_MS = Math.max(60 * 1000, parseInt(process.env.AUTH_ATTEMPT_WINDOW_MS || `${15 * 60 * 1000}`, 10));
const AUTH_ATTEMPT_MAX = Math.max(1, parseInt(process.env.AUTH_ATTEMPT_MAX || '5', 10));
const AUTH_BLOCK_BASE_MS = Math.max(30 * 1000, parseInt(process.env.AUTH_BLOCK_BASE_MS || `${5 * 60 * 1000}`, 10));
const AUTH_BLOCK_MAX_MS = Math.max(AUTH_BLOCK_BASE_MS, parseInt(process.env.AUTH_BLOCK_MAX_MS || `${60 * 60 * 1000}`, 10));
const limiterStore = new Map();
let lastPruneAt = 0;
const getClientIp = (req) => {
const forwarded = req.headers['x-forwarded-for']?.split(',')[0]?.trim();
const rawIp = forwarded || req.ip || req.connection?.remoteAddress || 'unknown';
return rawIp.replace('::ffff:', '');
};
const hashValue = (value) =>
crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 16);
const getIdentity = (req, mode) => {
if (mode === 'token') {
const token = (req.body?.token || '').trim();
return token ? `token:${hashValue(token)}` : 'token:anonymous';
}
const username = (req.body?.username || req.body?.email || '').trim().toLowerCase();
return username ? `acct:${hashValue(username)}` : 'acct:anonymous';
};
const getLimiterKey = (req, mode) => `${mode}:${getIdentity(req, mode)}:ip:${getClientIp(req)}`;
const getOrInitRecord = (key, now) => {
const existing = limiterStore.get(key);
if (existing) {
return existing;
}
const record = {
count: 0,
windowStartedAt: now,
blockedUntil: 0,
blockLevel: 0,
};
limiterStore.set(key, record);
return record;
};
const computeBlockMs = (blockLevel) =>
Math.min(AUTH_BLOCK_BASE_MS * (2 ** Math.max(0, blockLevel - 1)), AUTH_BLOCK_MAX_MS);
const authRateLimiter = (mode) => (req, res, next) => {
const now = Date.now();
if (now - lastPruneAt > 5 * 60 * 1000) {
for (const [storeKey, storeValue] of limiterStore.entries()) {
const isWindowExpired = now - storeValue.windowStartedAt > AUTH_ATTEMPT_WINDOW_MS;
const isNotBlocked = storeValue.blockedUntil <= now;
if (isWindowExpired && isNotBlocked) {
limiterStore.delete(storeKey);
}
}
lastPruneAt = now;
}
const key = getLimiterKey(req, mode);
const record = getOrInitRecord(key, now);
if (now - record.windowStartedAt > AUTH_ATTEMPT_WINDOW_MS) {
record.count = 0;
record.windowStartedAt = now;
}
if (record.blockedUntil > now) {
const retryAfterSec = Math.ceil((record.blockedUntil - now) / 1000);
res.set('Retry-After', retryAfterSec.toString());
client_logger.capture({
distinctId: 'app_level',
event: 'security@auth@rate_limited',
properties: {
route: req.originalUrl,
method: req.method,
mode,
keyHash: hashValue(key),
retryAfterSec,
blockLevel: record.blockLevel,
}
});
return res.status(429).json({ status: 'Too many attempts. Please try again later.' });
}
record.count += 1;
if (record.count > AUTH_ATTEMPT_MAX) {
record.blockLevel += 1;
const blockMs = computeBlockMs(record.blockLevel);
record.blockedUntil = now + blockMs;
record.count = 0;
record.windowStartedAt = now;
res.set('Retry-After', Math.ceil(blockMs / 1000).toString());
client_logger.capture({
distinctId: 'app_level',
event: 'security@auth@rate_limited',
properties: {
route: req.originalUrl,
method: req.method,
mode,
keyHash: hashValue(key),
retryAfterSec: Math.ceil(blockMs / 1000),
blockLevel: record.blockLevel,
}
});
return res.status(429).json({ status: 'Too many attempts. Please try again later.' });
}
return next();
};
module.exports = {
authRateLimiter,
};

View File

@@ -2,19 +2,30 @@ const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils
const { client_logger } = require('../utils/analyticsLogger');
const { cookiesOptions } = require('../config/cookiesOptions');
const MongoDB = require("../mongoDB.js");
const { ObjectId } = require("mongodb");
const sessionChecker = async (req, res, next) => {
try {
const session_id = getSessionId(req);
const user_sid = getUserId(req);
let profile_id = getProfileId(req);
if (session_id && user_sid) {
DB = await MongoDB.getDB;
if (!session_id || !user_sid) {
return res.redirect('/login');
}
if (!ObjectId.isValid(session_id) || !ObjectId.isValid(user_sid)) {
return res.redirect('/login');
}
const DB = await MongoDB.getDB;
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);
if (!latestProfile || !latestProfile._id) {
return res.redirect('/login');
}
res.cookie('profile_id', latestProfile._id, cookiesOptions);
profile_id = latestProfile._id;
}
@@ -23,14 +34,14 @@ const sessionChecker = async (req, res, next) => {
if (!userInfo) return res.redirect('/login');
// Log Request
client_logger.capture({
distinctId: user_sid,
event: 'server@' + req.method + '@' + req.originalUrl,
});
next();
} else {
} catch (error) {
console.error("Session checker error", error);
return res.redirect('/login');
}
};

View File

@@ -11,19 +11,39 @@ const paymentDB = require("./dbTools/payments.js");
const songsDB = require("./dbTools/songs.js");
console.log("Connecting to MongoDB...");
const nodeMajorVersion = parseInt((process.versions.node || "0").split(".")[0], 10);
if (nodeMajorVersion >= 22) {
console.warn("Warning: mongodb@3.x is not fully tested on Node.js 22+. Prefer Node.js 20 LTS for local stability.");
}
const mongoConnectOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 10000,
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
keepAlive: true,
};
const getDB = new Promise((resolve, reject) => {
const DB = {ObjectID: mongo.ObjectID};
MongoClient.connect(mongoUrl, function(err, db) {
MongoClient.connect(mongoUrl, mongoConnectOptions, function(err, db) {
if (err) return reject(err);
console.log("Connected to DB!");
DB.db = db;
DB.ObjectID = ObjectID;
DB.db.on("close", () => console.error("MongoDB connection closed"));
DB.db.on("reconnect", () => console.log("MongoDB reconnected"));
DB.db.on("error", (error) => console.error("MongoDB connection error", error));
DB.usersCol = db.db(DBName).collection("users");
DB.tokensCol = db.db(DBName).collection("tokens");
DB.invitationCol = db.db(DBName).collection("invitation");
DB.passwordLoginTokensCol = db.db(DBName).collection("password_login_tokens");
DB.passwordLoginTokensCol.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error);
DB.passwordLoginTokensCol.createIndex({ tokenHash: 1 }, { unique: true }).catch(console.error);
DB.checkSessionOnDB = async (session_id, user_sid)=>{
const temp_id = new mongo.ObjectID(session_id);
@@ -31,7 +51,7 @@ const getDB = new Promise((resolve, reject) => {
const doc = await DB.tokensCol.findOne({"_id":temp_id});
if(doc && doc.uid == user_sid){
const userMongoId = new mongo.ObjectID(user_sid);
const userInfo = await DB.usersCol.findOne({"_id": userMongoId}, {fields: {password: 0}});
const userInfo = await DB.usersCol.findOne({"_id": userMongoId}, {projection: {password: 0}});
return userInfo;
}
return false;
@@ -61,6 +81,42 @@ const getDB = new Promise((resolve, reject) => {
return DB.usersCol.findOne({ _id });
}
DB.createPasswordLoginToken = async (userId, tokenHash, expiresAt) => {
const userObjectId = mongo.ObjectID.isValid(userId) ? new mongo.ObjectID(userId) : userId;
const tokenDoc = {
userId: userObjectId,
tokenHash,
createdAt: new Date(),
expiresAt,
usedAt: null,
};
return DB.passwordLoginTokensCol.insertOne(tokenDoc).catch((err) => {
console.log(err);
return false;
});
};
DB.consumePasswordLoginToken = async (tokenHash) => {
const now = new Date();
const result = await DB.passwordLoginTokensCol.findOneAndUpdate(
{
tokenHash,
usedAt: null,
expiresAt: { $gt: now }
},
{
$set: { usedAt: now }
},
{
returnOriginal: false
}
).catch((err) => {
console.log(err);
return false;
});
return result?.value || null;
};
let usernamesCache = {}
DB.getUsernameByIdCache = async (userid)=>{
if(!userid) return {};

View File

@@ -347,7 +347,7 @@ const Notifications = {
const bookedProfile = subscribed[index];
if (bookedProfile._id == senderProfile._id) return 0;
const notifBody = `${senderProfile.profile.firstName} commented in a post you follow`;
sendPushNotification(bookedProfile.token, notifBody, {});
sendPushNotification(bookedProfile.token, notifBody, {post_id: post._id});
DB.addNotification(bookedProfile._id, notifBody, post._id, post.comments.length - 1, senderProfile._id);
yourBookmarkedPostGotACommentTemplate(post, userEmail, postProfile, senderProfile, bookedProfile, message);
});
@@ -363,7 +363,7 @@ const Notifications = {
}
if (postProfile.isCourse || senderProfile._id == postProfile._id) return 0; //Course owners do not need to receive notifs
const notifBody = `${senderProfile.profile.firstName} commented in your post`;
sendPushNotification(postProfile.token, notifBody, {});
sendPushNotification(postProfile.token, notifBody, {post_id: postId, comment: message});
DB.addNotification(post.profileid, notifBody, postId, post.comments.length - 1, senderProfile._id);
return youGotANewPostCommentTemplate(post, userEmail, postProfile, senderProfile, message);
},
@@ -376,7 +376,7 @@ const Notifications = {
subscribed.forEach((bookedProfile) => {
if (bookedProfile._id == senderProfile._id) return 0;
const notifBody = `${senderProfile.profile.firstName} liked a post you follow`;
sendPushNotification(bookedProfile.token, notifBody, {});
sendPushNotification(bookedProfile.token, notifBody, {post_id: post._id});
DB.addNotification(bookedProfile._id, notifBody, post._id, null, senderProfile._id);
});
},
@@ -391,7 +391,7 @@ const Notifications = {
}
if (postProfile.isCourse || senderProfile._id == postProfile._id) return 0; //Course owners do not need to receive notifs
const notifBody = `${senderProfile.profile.firstName} liked your post`;
sendPushNotification(postProfile.token, notifBody, {});
sendPushNotification(postProfile.token, notifBody, {post_id: post._id});
DB.addNotification(post.profileid, notifBody, postId, null, senderProfile._id);
return 0;
},
@@ -411,11 +411,11 @@ const Notifications = {
if (userProfile._id == senderProfile._id) return 0; //avoid sending self notifications
if(groupProfile._id == senderProfile._id){
const notifBody = `${groupProfile.profile.firstName} ${groupProfile.profile.lastName} has a new post!`;
sendPushNotification(userProfile.token, notifBody, {});
sendPushNotification(userProfile.token, notifBody, {post_id: post._id, profile_id: groupProfile._id});
return DB.addNotification(userProfile._id, notifBody, post._id, null, senderProfile._id);
}
const notifBody = `${senderProfile.profile.firstName} post in the group ${groupProfile.profile.firstName} ${groupProfile.profile.lastName}`;
sendPushNotification(userProfile.token, notifBody, {});
sendPushNotification(userProfile.token, notifBody, {post_id: post._id, profile_id: groupProfile._id});
DB.addNotification(userProfile._id, notifBody, post._id, null, senderProfile._id);
// Disabling email notifications for now, until settings are implemented
// yourGroupGotANewPostTemplate(groupProfile, userEmail, userProfile, senderProfile, message);
@@ -437,7 +437,7 @@ const Notifications = {
return this.broadcastNews(post, emails);
}
const notifBody = `${senderProfile.profile.firstName} post in your profile`;
sendPushNotification(profile.token, notifBody, {});
sendPushNotification(profile.token, notifBody, {post_id: post._id, profile_id: post.profileid});
// sendWebNotification(profile.webSubscription, notifBody, message);
DB.addNotification(toProfileId, notifBody, post._id, null, senderProfile._id);
return youGotANewPostTemplate(profile, user.username, senderProfile, message);
@@ -461,7 +461,7 @@ const Notifications = {
const userProfile = subscribed_profiles[index];
if (userProfile._id == whoPostedId._id) return 0;
const notifBody = `${profile.profile.firstName} posted: ${message.substring(0, 50)}...`;
sendPushNotification(userProfile.token, notifBody, {});
sendPushNotification(userProfile.token, notifBody, {profile_id: whoPostedId});
//sendWebNotification(userProfile.webSubscription, notifBody, message);
DB.addNotification(userProfile._id, notifBody, post._id, null, profile._id);
});

433
package-lock.json generated
View File

@@ -26,6 +26,8 @@
"posthog-node": "^4.4.1",
"socket.io": "^4.6.1",
"stripe": "^8.178.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"web-push": "^3.4.5"
},
"devDependencies": {
@@ -34,6 +36,50 @@
"supertest": "^7.0.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -137,6 +183,12 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"license": "MIT"
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
@@ -167,6 +219,13 @@
"node": ">=14"
}
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@@ -185,6 +244,12 @@
"@types/node": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
@@ -305,7 +370,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-flatten": {
@@ -516,6 +580,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"license": "MIT"
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -757,6 +827,15 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -983,6 +1062,18 @@
"node": ">=0.3.1"
}
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -1221,6 +1312,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1884,7 +1984,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -1928,6 +2027,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -2421,6 +2540,13 @@
"wrappy": "1"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT",
"peer": true
},
"node_modules/optional-require": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
@@ -3251,6 +3377,83 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"license": "MIT",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"license": "MIT",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.27.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.0.tgz",
"integrity": "sha512-tS6LRyBhY6yAqxrfsA9IYpGWPUJOri6sclySa7TdC7XQfGLvTwDY531KLgfQwHEtQsn+sT4JlUspbeQDBVGWig==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"license": "MIT",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/tar": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
@@ -3326,6 +3529,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/validator": {
"version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -3563,6 +3775,15 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -3668,9 +3889,73 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"license": "MIT",
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"bin": {
"z-schema": "bin/z-schema"
},
"engines": {
"node": ">=8.0.0"
},
"optionalDependencies": {
"commander": "^9.4.1"
}
},
"node_modules/z-schema/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.20.0 || >=14"
}
}
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"requires": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="
},
"@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
},
"@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"requires": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
}
},
"@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3736,6 +4021,11 @@
}
}
},
"@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
},
"@mapbox/node-pre-gyp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
@@ -3759,6 +4049,11 @@
"dev": true,
"optional": true
},
"@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="
},
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@@ -3777,6 +4072,11 @@
"@types/node": "*"
}
},
"@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"@types/node": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
@@ -3866,8 +4166,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"array-flatten": {
"version": "1.1.1",
@@ -4032,6 +4331,11 @@
"get-intrinsic": "^1.2.6"
}
},
"call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
},
"camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -4199,6 +4503,11 @@
"delayed-stream": "~1.0.0"
}
},
"commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q=="
},
"component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -4361,6 +4670,14 @@
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true
},
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"requires": {
"esutils": "^2.0.2"
}
},
"dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -4525,6 +4842,11 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -4979,7 +5301,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": {
"argparse": "^2.0.1"
}
@@ -5012,6 +5333,21 @@
"p-locate": "^5.0.0"
}
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
},
"log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -5346,6 +5682,12 @@
"wrappy": "1"
}
},
"openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"peer": true
},
"optional-require": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
@@ -5940,6 +6282,58 @@
"has-flag": "^4.0.0"
}
},
"swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"requires": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"requires": {
"@apidevtools/swagger-parser": "10.0.3"
}
},
"swagger-ui-dist": {
"version": "5.27.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.0.tgz",
"integrity": "sha512-tS6LRyBhY6yAqxrfsA9IYpGWPUJOri6sclySa7TdC7XQfGLvTwDY531KLgfQwHEtQsn+sT4JlUspbeQDBVGWig==",
"requires": {
"@scarf/scarf": "=1.4.0"
}
},
"swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"requires": {
"swagger-ui-dist": ">=5.0.0"
}
},
"tar": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
@@ -5996,6 +6390,11 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"validator": {
"version": "13.15.15",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -6154,6 +6553,11 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ=="
},
"yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -6226,6 +6630,25 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"z-schema": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"requires": {
"commander": "^9.4.1",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^13.7.0"
},
"dependencies": {
"commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"optional": true
}
}
}
}
}

View File

@@ -30,6 +30,8 @@
"posthog-node": "^4.4.1",
"socket.io": "^4.6.1",
"stripe": "^8.178.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"web-push": "^3.4.5"
},
"devDependencies": {

View File

@@ -13,11 +13,47 @@ const defaultBibleId = "592420522e16049f-01";
//getMedia('y42zyf3').then(console.log)
DB.getDB.then((DB) => {
/**
* @swagger
* tags:
* name: Bible
* description: Bible API
*/
/**
* @swagger
* /bible:
* get:
* summary: Get a list of available Bibles
* tags: [Bible]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
*/
router.get("", async (req, res) => {
const bibles = await fetchAPI('bibles');
return res.json(bibles);
});
/**
* @swagger
* /bible/books:
* get:
* summary: Get the books of a Bible
* tags: [Bible]
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: bibleId
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/books", async (req, res) => {
const bibleId = req.query.bibleId || defaultBibleId;
const bibles = await fetchAPI('bibles/' + bibleId +"/books");
@@ -30,12 +66,56 @@ DB.getDB.then((DB) => {
return res.json(bibles);
});
/**
* @swagger
* /bible/books/{bookId}:
* get:
* summary: Get details for a specific book
* tags: [Bible]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: bookId
* required: true
* schema:
* type: string
* - in: query
* name: bibleId
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/books/:bookId", async (req, res) => {
const bookId = req.params.bookId;
const bibles = await fetchAPI('bibles/' + bibleId +"/books/" + bookId);
return res.json(bibles);
});
/**
* @swagger
* /bible/books/{bookId}/chapters:
* get:
* summary: Get the chapters of a book
* tags: [Bible]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: bookId
* required: true
* schema:
* type: string
* - in: query
* name: bibleId
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/books/:bookId/chapters", async (req, res) => {
const bookId = req.params.bookId;
const bibleId = req.query.bibleId || defaultBibleId;
@@ -43,6 +123,28 @@ DB.getDB.then((DB) => {
return res.json(bibles);
});
/**
* @swagger
* /bible/chapters/{chapterId}:
* get:
* summary: Get the content of a chapter
* tags: [Bible]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: chapterId
* required: true
* schema:
* type: string
* - in: query
* name: bibleId
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/chapters/:chapterId", async (req, res) => {
const chapterId = req.params.chapterId;
const bibleId = req.query.bibleId || defaultBibleId;
@@ -50,6 +152,28 @@ DB.getDB.then((DB) => {
return res.json(bibles);
});
/**
* @swagger
* /bible/chapters/{chapterId}/verses:
* get:
* summary: Get the verses of a chapter
* tags: [Bible]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: chapterId
* required: true
* schema:
* type: string
* - in: query
* name: bibleId
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/chapters/:chapterId/verses", async (req, res) => {
const chapterId = req.params.chapterId;
const bibleId = req.query.bibleId || defaultBibleId;
@@ -57,6 +181,33 @@ DB.getDB.then((DB) => {
return res.json(bibles);
});
/**
* @swagger
* /bible/search:
* get:
* summary: Search the Bible
* tags: [Bible]
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: query
* required: true
* schema:
* type: string
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* - in: query
* name: bibleId
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/search", async (req, res) => {
const query = req.query.query;
const limit = req.query.limit || 10;

View File

@@ -2,6 +2,7 @@ var express = require('express');
var router = express.Router();
const DB = require("../mongoDB.js");
const mongo = require('mongodb');
//const Payments = require("../payments.js");
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE);
@@ -38,6 +39,13 @@ DB.getDB.then((DB) => {
// });
// });
/**
* @swagger
* tags:
* name: Payments
* description: Payment processing
*/
let intent = async (req, res) => {
const userid = req.body.userid;
const price = req.body.price || 500;
@@ -52,6 +60,15 @@ DB.getDB.then((DB) => {
],
});
// check if user is email or userid
const isUserId = mongo.ObjectId.isValid(userid);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isEmail = emailRegex.test(userid.trim().toLowerCase());
console.log("isUserId: ", isUserId);
console.log("isEmail: ", isEmail);
console.log("userid: ", userid);
if (isUserId) {
//Register in DB
const intent = {
paymentIntent,
@@ -67,12 +84,93 @@ DB.getDB.then((DB) => {
email: await DB.getUsernameByIdCache(userid),
price
});
}
if (isEmail) {
//Register in DB
return res.send({
clientSecret: paymentIntent.client_secret,
email: userid,
price
});
}
return res.send({
clientSecret: paymentIntent.client_secret,
email: 'guess',
price
});
};
/**
* @swagger
* /payments/create-payment-intent:
* post:
* summary: Creates a Stripe Payment Intent
* tags: [Payments]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* userid:
* type: string
* price:
* type: number
* description:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/create-payment-intent", intent);
/**
* @swagger
* /payments/intent:
* post:
* summary: Creates a Stripe Payment Intent (Alias for /create-payment-intent)
* tags: [Payments]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* userid:
* type: string
* price:
* type: number
* description:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/intent", intent);
/**
* @swagger
* /payments/register:
* post:
* summary: Registers a payment after a successful Stripe transaction
* tags: [Payments]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* userid:
* type: string
* result:
* type: object
* responses:
* 200:
* description: OK
*/
router.post("/register", async (req, res) => {
const userid = req.body.userid;
const result = req.body.result;
@@ -84,7 +182,7 @@ DB.getDB.then((DB) => {
};
//console.log(payment);
const intent = await DB.getIntent(result.client_secret);
if(intent.description === "Subscription 1 Month"){
if (intent.description === "Subscription 1 Month") {
//update profile subscription status
const profileid = getProfileId(req);
const isSubscriptor = await DB.isSubscriptor(profileid);

View File

@@ -8,7 +8,10 @@ const Notifications = require("./../notifications.js");
DB.getDB.then((DB) => {
const getProfileId = (req) => {
return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id);
const rawProfileId = req.cookies.profile_id || req.query.profile_id || req.body.profile_id || req.profileInfo?._id;
if (!rawProfileId) return null;
if (!DB.ObjectID.isValid(rawProfileId)) return null;
return DB.ObjectID(rawProfileId);
};
const postBelongToProfile = (post, profileid) => {
@@ -77,24 +80,193 @@ DB.getDB.then((DB) => {
return mergedPosts;
};
/**
* @swagger
* tags:
* name: Posts
* description: Post management
*/
/**
* @swagger
* components:
* schemas:
* Post:
* type: object
* properties:
* profileid:
* type: string
* description: The ID of the profile that created the post.
* content:
* type: string
* description: The content of the post.
* createdAt:
* type: string
* format: date-time
* description: The timestamp when the post was created.
* reactions:
* type: object
* description: An object containing reactions to the post, keyed by profile ID.
* comments:
* type: array
* items:
* type: object
* description: An array of comments on the post.
* bookmarks:
* type: array
* items:
* type: string
* description: An array of profile IDs that bookmarked the post.
* nonOrganicType:
* type: string
* description: Type of non-organic content (e.g., "News", "PopularUsers", "PopularGroups").
* contentHistory:
* type: array
* items:
* type: string
* description: A history of content edits.
* toProfile:
* type: string
* description: The ID of the profile this post is directed to (e.g., a group).
* lastUpdated:
* type: string
* format: date-time
* description: The timestamp when the post was last updated.
* tags:
* type: array
* items:
* type: string
* description: An array of tags associated with the post.
* chatSenderId:
* type: string
* description: The ID of the chat sender, if applicable.
*
* /post/organic:
* get:
* summary: Get the organic feed for the current user
* tags: [Posts]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/organic", async (req, res) => {
try {
const profileid = getProfileId(req);
if (!profileid) return res.status(400).json([]);
let organicPosts = await DB.getFeed(profileid);
//Add non-organic posts
const nonOrganicPosts = await generateNonOrganicPosts(req, profileid);
const posts = mergePosts(organicPosts, nonOrganicPosts);
const posts = mergePosts(organicPosts || [], nonOrganicPosts || []);
return res.json(posts);
} catch (error) {
console.error("Error loading organic feed", error);
return res.status(500).json([]);
}
});
/**
* @swagger
* /post:
* get:
* summary: Get the feed with promotional content
* tags: [Posts]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/", async (req, res) => {
try {
const profileid = getProfileId(req);
//Add non-organic posts
if (!profileid) return res.status(400).json([]);
const nonOrganicPosts = await generateNonOrganicPosts(req, profileid);
let promotionalPosts = await DB.getPromotionalPosts(profileid);
const posts = mergePosts(promotionalPosts, nonOrganicPosts);
const posts = mergePosts(promotionalPosts || [], nonOrganicPosts || []);
return res.json(posts);
} catch (error) {
console.error("Error loading feed", error);
return res.status(500).json([]);
}
});
/**
* @swagger
* /post/tag/{tag}:
* get:
* summary: Get posts with a specific tag
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: tag
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Post'
* 400:
* description: Tag is required
*/
router.get("/tag/:tag", async (req, res) => {
const profileid = getProfileId(req);
const tag = req.query.tag || req.params.tag;
if(!tag) {
console.log("Tag query empty: ", tag);
return res.json({
status: "Tag is required",
});
}
console.log("Tag query: ", tag);
let posts = await DB.getPostsByTag('#' + tag, profileid);
return res.json(posts);
});
/**
* @swagger
* /post/usr/{id}:
* get:
* summary: Get posts from a specific user
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/usr/:id", async (req, res) => {
const profileId = req.params.id;
const viewerProdileId = getProfileId(req);
@@ -110,6 +282,35 @@ DB.getDB.then((DB) => {
return res.json(posts);
});
/**
* @swagger
* /post/usr/{id}/images:
* get:
* summary: Get all image posts from a user
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* posts:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/usr/:id/images", async (req, res) => {
const profileid = req.params.id;
const viewerProfileId = getProfileId(req);
@@ -120,6 +321,35 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/usr/{id}/embedded:
* get:
* summary: Get all embedded posts from a user
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* posts:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/usr/:id/embedded", async (req, res) => {
const profileid = req.params.id;
const viewerProfileId = getProfileId(req);
@@ -130,6 +360,35 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/usr/{id}/media:
* get:
* summary: Get all media posts from a user
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* posts:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/usr/:id/media", async (req, res) => {
const profileid = req.params.id;
const viewerProfileId = getProfileId(req);
@@ -140,11 +399,49 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/video/{id}:
* get:
* summary: Get video details by ID
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/video/:id", async (req, res) => {
videoId = req.params.id;
return res.json([]);
});
/**
* @swagger
* /post:
* post:
* summary: Create a new post
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: OK
* 403:
* description: Not authorized to post to this group
*/
router.post("/", async (req, res) => {
let post = {
profileid: getProfileId(req),
@@ -184,6 +481,27 @@ DB.getDB.then((DB) => {
})
});
/**
* @swagger
* /post/react:
* post:
* summary: React to a post
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/react", async (req, res) => {
let profileid = getProfileId(req);
let postid = req.body.postid;
@@ -198,6 +516,27 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/unreact:
* post:
* summary: Remove a reaction from a post
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/unreact", async (req, res) => {
let profileid = getProfileId(req);
let postid = req.body.postid;
@@ -207,6 +546,27 @@ DB.getDB.then((DB) => {
})
});
/**
* @swagger
* /post/bookmark:
* post:
* summary: Bookmark a post
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/bookmark", async (req, res) => {
let profileid = getProfileId(req);
let postid = req.body.postid;
@@ -216,6 +576,27 @@ DB.getDB.then((DB) => {
});
})
/**
* @swagger
* /post/unbookmark:
* post:
* summary: Remove a bookmark from a post
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/unbookmark", async (req, res) => {
let profileid = getProfileId(req);
let postid = req.body.postid;
@@ -225,6 +606,29 @@ DB.getDB.then((DB) => {
})
});
/**
* @swagger
* /post/comment:
* post:
* summary: Add a comment to a post
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* content:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/comment/", async (req, res) => {
let profileid = getProfileId(req);
let postid = req.body.postid;
@@ -244,6 +648,30 @@ DB.getDB.then((DB) => {
})
});
/**
* @swagger
* /post/comment/react:
* post:
* summary: React to a comment
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* commentDate:
* type: string
* format: date-time
* responses:
* 200:
* description: OK
*/
router.post("/comment/react", async (req, res) => {
let userid = getProfileId(req);
let postid = req.body.postid;
@@ -258,6 +686,30 @@ DB.getDB.then((DB) => {
})
});
/**
* @swagger
* /post/comment/unreact:
* post:
* summary: Remove a reaction from a comment
* tags: [Posts]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* postid:
* type: string
* commentDate:
* type: string
* format: date-time
* responses:
* 200:
* description: OK
*/
router.post("/comment/unreact", async (req, res) => {
let profileid = getProfileId(req);
let postid = req.body.postid;
@@ -268,6 +720,29 @@ DB.getDB.then((DB) => {
})
});
/**
* @swagger
* /post/images:
* get:
* summary: Get all image posts for the current user
* tags: [Posts]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* posts:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/images", async (req, res) => {
const profileid = getProfileId(req);
const posts = await DB.getMediaTagPostOfUser(profileid, profileid);
@@ -277,6 +752,29 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/embedded:
* get:
* summary: Get all embedded posts for the current user
* tags: [Posts]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* posts:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/embedded", async (req, res) => {
const profileid = getProfileId(req);
const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@iframe:");
@@ -286,6 +784,29 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/media:
* get:
* summary: Get all media posts for the current user
* tags: [Posts]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* posts:
* type: array
* items:
* $ref: '#/components/schemas/Post'
*/
router.get("/media", async (req, res) => {
const profileid = getProfileId(req);
const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@youtube:|@vimeo:|@hls:");
@@ -295,6 +816,74 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/course/recent:
* get:
* summary: Get recently watched media from courses
* tags: [Posts]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* recentMedia:
* type: object
* additionalProperties:
* type: array
* items:
* type: object
* properties:
* watchRecord:
* type: object
* _id:
* type: string
* profileid:
* type: string
* content:
* type: string
* createdAt:
* type: string
* format: date-time
* reactions:
* type: object
* comments:
* type: array
* items:
* type: object
* bookmarks:
* type: array
* items:
* type: string
* nonOrganicType:
* type: string
* contentHistory:
* type: array
* items:
* type: string
* toProfile:
* type: string
* lastUpdated:
* type: string
* format: date-time
* tags:
* type: array
* items:
* type: string
* chatSenderId:
* type: string
* mediaProfileMap:
* type: object
* additionalProperties:
* type: object
*/
router.get("/course/recent", async (req, res) => {
const profileid = getProfileId(req);
const profile = await DB.getProfileCache(profileid);
@@ -342,6 +931,28 @@ DB.getDB.then((DB) => {
});
});
/**
* @swagger
* /post/{id}:
* get:
* summary: Get a specific post by ID
* tags: [Posts]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Post'
*/
router.get("/:id", async (req, res) => {
const postId = req.params.id;
const post = await DB.getPost(postId);

View File

@@ -4,26 +4,112 @@ var router = express.Router()
const DB = require("../mongoDB.js");
const Profile = require("../def/profile.js");
const Notifications = require("./../notifications.js");
const { getSessionId, getUserId, getProfileId } = require("./../utils/sessionUtils.js");
DB.getDB.then((DB)=>{
const getUserId = function(req){
const user_sid = req.cookies.user_sid || req.query.user_sid || req.body.user_sid;
return DB.ObjectID(user_sid);
}
const getProfileId = (req)=>{
return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id);
}
DB.getDB.then((DB) => {
const profileBelongsToUser = async (profileid, userid) => {
const profile = await DB.getProfileCache(profileid);
if(!profile) return false;
return profile.userid == (userid + '');
if (!profile) return false;
return profile.userid === String(userid);
}
/**
* @swagger
* tags:
* name: Profiles
* description: User profile management
*/
/**
* @swagger
* components:
* schemas:
* Profile:
* type: object
* properties:
* userid:
* type: string
* description: The ID of the user associated with the profile.
* profile:
* type: object
* properties:
* firstName:
* type: string
* lastName:
* type: string
* photo:
* type: string
* location:
* type: string
* language:
* type: string
* status:
* type: string
* description:
* type: string
* data:
* type: object
* description: Additional custom data for the profile.
* username:
* type: string
* following:
* type: array
* items:
* type: string
* description: List of profile IDs this profile is following.
* lastUpdate:
* type: string
* format: date-time
* newsFeedCache:
* type: array
* items:
* type: object
* notifications:
* type: array
* items:
* type: object
* isGroup:
* type: boolean
* isCourse:
* type: boolean
* isPrivate:
* type: boolean
* isChat:
* type: boolean
* subscribed:
* type: object
* description: Users subscribed to this group (if isGroup is true).
* pending:
* type: object
* description: Pending subscription requests for private groups.
*/
/**
* @swagger
* /user/mine:
* get:
* summary: Get all profiles for the logged-in user
* tags: [Profiles]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* profiles:
* type: array
* items:
* $ref: '#/components/schemas/Profile'
*/
router.get("/mine", async (req, res) => {
let userid = req.cookies.user_sid;
let userid = getUserId(req);
let profiles = await DB.getUserProfiles(userid);
return res.json({
status: "ok",
@@ -31,64 +117,212 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/new:
* get:
* summary: (DEPRECATED) Create a new profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: content
* required: true
* schema:
* type: object
* $ref: '#/components/schemas/Profile'
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Profile'
*/
router.get("/new", async (req, res) => { //Deprecated please use route post("/")
let profile = {
userid: getUserId(req),
... req.query.content
...req.query.content
};
let profileObj = new Profile(profile);
let r = await DB.newProfile(profileObj);
return res.json({
status: "ok",
... profileObj.toObj()
...profileObj.toObj()
});
});
/**
* @swagger
* /user:
* post:
* summary: Create a new profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Profile'
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Profile'
*/
router.post("/", async (req, res) => {
let profile = {
userid: getUserId(req),
... req.body.content
...req.body.content
};
try {
let profileObj = new Profile(profile);
let r = await DB.newProfile(profileObj);
return res.json({
status: "ok",
... profileObj.toObj()
...profileObj.toObj()
});
});
router.post("/invite", async (req, res) => {
const userid = getUserId(req);
const name = req.body.name;
const email = req.body.email;
//validate email?
if(!name || !email) return res.json({status: "incomplete request"});
let r = await DB.newInvitation(userid, name, email);
if(!r.toLowerCase){
//send email invitation
let senderProfile = await DB.getProfile(getProfileId(req));
Notifications.youHaveAnInvitation(name, email, senderProfile);
} catch (error) {
console.error("Error creating profile", error);
return res.json({
status: "ok"
status: error,
});
}
return res.json({
status: r
});
});
/**
* @swagger
* /user/invite:
* post:
* summary: Invite a new user by email
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* email:
* type: string
* format: email
* responses:
* 200:
* description: OK
* 400:
* description: Bad request
*/
router.post("/invite", async (req, res) => {
try {
const userid = getUserId(req);
let { name, email } = req.body; // Destructuring for clarity
// Validate required fields
if (!name || !email) {
return res.status(400).json({ status: "Name and email are required" });
}
// Validate email format
email = email.trim().toLowerCase()
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ status: "Invalid email format" });
}
// Create new invitation, this returns a string if failed
let r = await DB.newInvitation(userid, name, email);
if (r instanceof String) {
// Handle failure response from DB.newInvitation
return res.status(400).json({
status: r,
message: `Failed to send invitation: ${r}`
});
}
// Handle response from DB.newInvitation
// Send email invitation
let senderProfile = await DB.getProfile(getProfileId(req));
Notifications.youHaveAnInvitation(name, email, senderProfile);
return res.status(200).json({
status: "ok",
message: `Invitation sent to ${name} (${email})`
});
} catch (error) {
console.error("Error during invitation process:", error);
return res.status(500).json({ status: "error", message: "Something went wrong, please try again later" });
}
});
/**
* @swagger
* /user/invite/{email}:
* get:
* summary: Get invitation details for an email
* tags: [Profiles]
* security:
* - cookieAuth: []
* 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:
* $ref: '#/components/schemas/Profile'
*
*/
router.get("/invite/:email", async (req, res) => {
const userid = getUserId(req);
const email = req.params.email;
//validate email?
if(!email) return res.json({status: "provide valid email"});
if (!email) return res.json({ status: "provide valid email" });
let r = await DB.getInvitation(email);
if(!r) return res.json({status: "no invitation found with that email"});
if (!r) return res.json({ status: "no invitation found with that email" });
let isUserAlreadyRegistered = await DB.getUser(email);
if(isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({status: "This user is already registered"});
return res.json({status: "ok", ... r});
if (isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({ status: "This user is already registered" });
return res.json({ status: "ok", ...r });
});
/**
* @swagger
* /user/groups:
* get:
* summary: Get a list of all groups
* tags: [Profiles]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* groups:
* type: array
* items:
* $ref: '#/components/schemas/Profile'
*/
router.get("/groups", async (req, res) => {
let groups = await DB.getGroups();
return res.json({
@@ -97,6 +331,29 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups/following:
* get:
* summary: Get a list of groups the current profile is following
* tags: [Profiles]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* groups:
* type: array
* items:
* $ref: '#/components/schemas/Profile'
*/
router.get("/groups/following", async (req, res) => {
const profileId = getProfileId(req);
let groups = await DB.getFollowingGroups(profileId);
@@ -106,20 +363,65 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups:
* post:
* summary: Create a new group
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Profile'
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Profile'
*/
router.post("/groups", async (req, res) => {
let profile = {
userid: getUserId(req),
isGroup: true,
... req.body
...req.body
};
let profileObj = new Profile(profile);
DB.newProfile(profileObj)
return res.json({
status: "ok",
... profileObj.toObj()
...profileObj.toObj()
});
});
/**
* @swagger
* /user/courses:
* get:
* summary: Get a list of all courses
* tags: [Profiles]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* groups:
* type: array
* items:
* $ref: '#/components/schemas/Profile'
*/
router.get("/courses", async (req, res) => {
let groups = await DB.getCourses();
return res.json({
@@ -128,12 +430,35 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups/accept:
* post:
* summary: Accept a request to join a private group
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* groupid:
* type: string
* profileid:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/groups/accept", async (req, res) => {
//This function should be called to accept the join request
//of an user that attempt to join a private group.
const groupid = getProfileId(req); //It needs to have this profile context
const groupidBody = req.body.groupid ? DB.ObjectID(req.body.groupid) : undefined;
if(groupidBody && groupid != groupidBody && !DB.isOwnerOfGroup(groupid, groupidBody)){
if (groupidBody && groupid != groupidBody && !DB.isOwnerOfGroup(groupid, groupidBody)) {
return res.json({
status: "Only group owner can accept new subscribers"
});
@@ -147,12 +472,35 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups/reject:
* post:
* summary: Reject a request to join a private group
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* groupid:
* type: string
* profileid:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/groups/reject", async (req, res) => {
//This function should be called to reject the join request
//of an user that attempt to join a private group.
const groupid = getProfileId(req); //It needs to have this profile context
const groupidBody = req.body.groupid ? DB.ObjectID(req.body.groupid) : undefined;
if(groupidBody && groupid != groupidBody && !DB.isOwnerOfGroup(groupid, groupidBody)){
if (groupidBody && groupid != groupidBody && !DB.isOwnerOfGroup(groupid, groupidBody)) {
return res.json({
status: "Only group owner can reject new subscribers"
});
@@ -166,6 +514,39 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups/search:
* get:
* summary: Search for groups
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: query
* required: true
* schema:
* type: string
* - in: query
* name: courses
* schema:
* type: boolean
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* groups:
* type: array
* items:
* $ref: '#/components/schemas/Profile'
*/
router.get("/groups/search", async (req, res) => {
let query = req.query.query;
let coursesB = req.query.courses ? true : false;
@@ -176,6 +557,33 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups/{id}:
* get:
* summary: Get details for a specific group
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* groups:
* $ref: '#/components/schemas/Profile'
*/
router.get("/groups/:id", async (req, res) => {
const groupid = req.params.id;
let groups = await DB.getGroup(groupid);
@@ -185,18 +593,54 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/groups/{id}/subscribe:
* get:
* summary: Subscribe to a group
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/groups/:id/subscribe", async (req, res) => {
const groupid = req.params.id;
const profileid = getProfileId(req);
const isPrivate = await DB.isGroupPrivate(groupid);
DB.subscribeToGroup(profileid, groupid, isPrivate);
//Add notification to group owner
if(isPrivate) Notifications.yourGroupHasARequest(profileid, groupid)
if (isPrivate) Notifications.yourGroupHasARequest(profileid, groupid)
return res.json({
status: "ok"
});
});
/**
* @swagger
* /user/groups/{id}/unsubscribe:
* get:
* summary: Unsubscribe from a group
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/groups/:id/unsubscribe", async (req, res) => {
const groupid = req.params.id;
const profileid = getProfileId(req);
@@ -207,6 +651,35 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/search:
* get:
* summary: Search for profiles
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: query
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* profiles:
* type: array
* items:
* $ref: '#/components/schemas/Profile'
*/
router.get("/search", async (req, res) => {
let query = req.query.query;
let profiles = await DB.searchProfile(query);
@@ -216,6 +689,36 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/setData:
* post:
* summary: Set custom data for a profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* key:
* type: string
* value:
* type: object
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
*/
router.post("/setData", (req, res) => {
const key = req.body.key;
const value = req.body.value;
@@ -226,6 +729,36 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/myProfile:
* post:
* summary: Update the current user's profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* profile:
* $ref: '#/components/schemas/Profile'
* data:
* type: object
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
*/
router.post("/myProfile", async (req, res) => {
let profile = {
userid: getUserId(req),
@@ -239,19 +772,78 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/{id}:
* get:
* summary: Get a specific profile by ID
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Profile'
*/
router.get("/:id", async (req, res) => {
try {
let profileId = req.params.id;
let profile = await DB.getProfile(profileId);
if (!profile || !profile._id) {
return res.status(404).json({
status: "Profile not found",
});
}
return res.json({
status: "ok",
... profile
...profile
});
} catch (error) {
console.error("Error loading profile", error);
return res.status(500).json({
status: "Internal server error"
});
}
});
/**
* @swagger
* /user/{id}:
* delete:
* summary: Delete a profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
*/
router.delete("/:id", async (req, res) => {
const profileId = req.params.id;
const userid = getUserId(req);
if(!await profileBelongsToUser(profileId, userid))
if (!await profileBelongsToUser(profileId, userid))
return res.json({
status: "This profile is not yours."
});
@@ -261,6 +853,31 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/{id}/follow:
* get:
* summary: Follow a profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
*/
router.get("/:id/follow", async (req, res) => {
let followProfileId = req.params.id;
const profileid = getProfileId(req);
@@ -271,6 +888,31 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /user/{id}/unfollow:
* get:
* summary: Unfollow a profile
* tags: [Profiles]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
*/
router.get("/:id/unfollow", async (req, res) => {
let followProfileId = req.params.id;
const profileid = getProfileId(req);

View File

@@ -15,6 +15,25 @@ DB.getDB.then((DB)=>{
return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id);
}
/**
* @swagger
* tags:
* name: Songs
* description: Song management
*/
/**
* @swagger
* /songs:
* get:
* summary: Get all songs
* tags: [Songs]
* security:
* - cookieAuth: []
* responses:
* 200:
* description: OK
*/
router.get("/", async (req, res) => {
let profileId = req.params.id;
let songs = await DB.getSongs();
@@ -24,6 +43,24 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /songs:
* post:
* summary: Create a new song
* tags: [Songs]
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: OK
*/
router.post("/", async (req, res) => {
let post = {
userid: getUserId(req),
@@ -39,6 +76,24 @@ DB.getDB.then((DB)=>{
})
});
/**
* @swagger
* /songs/{id}:
* get:
* summary: Get a specific song by ID
* tags: [Songs]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/:id", async (req, res) => {
let profileId = req.params.id;
let profile = await DB.getProfile(profileId);
@@ -53,6 +108,24 @@ DB.getDB.then((DB)=>{
return true;
}
/**
* @swagger
* /songs/{id}:
* delete:
* summary: Delete a song
* tags: [Songs]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.delete("/:id", async (req, res) => {
const userid = getUserId(req);
const songId = req.params.id;
@@ -66,6 +139,33 @@ DB.getDB.then((DB)=>{
});
});
/**
* @swagger
* /songs/{id}:
* post:
* summary: Update a song
* tags: [Songs]
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* content:
* type: string
* responses:
* 200:
* description: OK
*/
router.post("/:id", async (req, res) => {
const userid = getUserId(req);
const songId = req.params.id;

View File

@@ -67,12 +67,51 @@ const getMedia = async (eventId) => {
};
//getMedia('y42zyf3').then(console.log)
/**
* @swagger
* tags:
* name: Subsplash
* description: Subsplash API integration
*/
DB.getDB.then((DB) => {
/**
* @swagger
* /subsplash/events/{calendarId}:
* get:
* summary: Get events from a Subsplash calendar
* tags: [Subsplash]
* parameters:
* - in: path
* name: calendarId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/events/:calendarId", async (req, res) => {
const events = await getEvents(req.params.calendarId)
return res.json(events);
});
/**
* @swagger
* /subsplash/media/{seriesId}:
* get:
* summary: Get media from a Subsplash media series
* tags: [Subsplash]
* parameters:
* - in: path
* name: seriesId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: OK
*/
router.get("/media/:seriesId", async (req, res) => {
const events = await getMedia(req.params.seriesId)
return res.json(events);

View File

@@ -1,14 +1,43 @@
const { ObjectId } = require("mongodb");
const isValidObjectId = (id) => ObjectId.isValid(id);
// Utilities
const getSessionId = function (req) {
const session_id = req.cookies.session_id || req.query.session_id || req.body.session_id;
if(!session_id) {
return session_id;
}
if(isValidObjectId(session_id)) {
return session_id;
}
console.trace();
console.error("Invalid session_id format: ", session_id);
return session_id;
}
const getUserId = function (req) {
const user_sid = req.cookies.user_sid || req.query.user_sid || req.body.user_sid;
// validate user_sid
if(!user_sid) {
return user_sid;
}
if(isValidObjectId(user_sid)) {
return user_sid;
}
console.trace();
console.error("Invalid user_sid format: ", user_sid);
return user_sid;
}
const getProfileId = function (req) {
const profile_id = req.cookies.profile_id || req.query.profile_id || req.body.profile_id;
if(!profile_id) {
return profile_id;
}
if(isValidObjectId(profile_id)) {
return profile_id;
}
console.trace();
console.error("Invalid profile_id format: ", profile_id);
return profile_id;
}