10 Commits

Author SHA1 Message Date
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
16 changed files with 2747 additions and 219 deletions

139
README.md
View File

@@ -2,8 +2,141 @@
This is the code for the backend of the EMI website. 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 ### TODO
[ ] Define nodes schema - [ ] Define nodes schema
[ ] Implement basic login/registration - [ ] Implement basic login/registration
[ ]

View File

@@ -74,13 +74,13 @@ const login = async function (req, res) {
// Check if user is already logged in and redirect to root if so. // Check if user is already logged in and redirect to root if so.
const session_id = getSessionId(req); const session_id = getSessionId(req);
const user_sid = getUserId(req); const user_sid = getUserId(req);
const DB = await MongoDB.getDB;
if (session_id && user_sid) { if (session_id && user_sid) {
const userInfo = await DB.checkSessionOnDB(session_id, user_sid); const userInfo = await DB.checkSessionOnDB(session_id, user_sid);
if (userInfo) return res.redirect('/'); if (userInfo) return res.redirect('/');
} }
const username = req.body.username || req.query.username; const username = req.body.username || req.query.username;
const password = req.body.password || req.query.password || ""; const password = req.body.password || req.query.password || "";
const DB = await MongoDB.getDB;
const user = await DB.getUser(username); const user = await DB.getUser(username);
if (!user) { if (!user) {
client_logger.capture({ client_logger.capture({

View File

@@ -233,7 +233,7 @@ postDB = (DB)=>{
if(!DB.ObjectID.isValid(profileId)) return []; if(!DB.ObjectID.isValid(profileId)) return [];
const profile = await DB.getProfile(profileId); const profile = await DB.getProfile(profileId);
if(!profile) return []; if(!profile) return [];
query = { const query = {
nonOrganicType: null // Exlcude news nonOrganicType: null // Exlcude news
}; };
return DB.postCols.find(query).sort({lastUpdated: -1}).limit(50).toArray().then(async (posts)=>{ 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 () => { DB.getNews = async () => {
let query = { let query = {
nonOrganicType: 'News' nonOrganicType: 'News'

View File

@@ -15,7 +15,7 @@ userDB = (DB) => {
DB.removeProfile = (profileid) => { DB.removeProfile = (profileid) => {
const _id = DB.ObjectID(profileid); const _id = DB.ObjectID(profileid);
if (userProfileCache[profileid]) delete userProfileCache[profileid]; if (userProfileCache[profileid]) delete userProfileCache[profileid];
return DB.profileCols.deleteOne({_id}).catch((err)=>{ return DB.profileCols.deleteOne({ _id }).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -23,7 +23,7 @@ userDB = (DB) => {
DB.updateProfile = async (profileid, profileObj) => { DB.updateProfile = async (profileid, profileObj) => {
let tempProfile = profileObj.toObj(); let tempProfile = profileObj.toObj();
const query = {_id: profileid}; const query = { _id: profileid };
const update = { const update = {
$set: { $set: {
profile: tempProfile.profile, profile: tempProfile.profile,
@@ -39,8 +39,8 @@ userDB = (DB) => {
DB.getProfile = async (profileId) => { DB.getProfile = async (profileId) => {
//if (userProfileCache[profileId] && !userProfileCache[profileId].isGroup) return userProfileCache[profileId]; //if (userProfileCache[profileId] && !userProfileCache[profileId].isGroup) return userProfileCache[profileId];
if(!profileId) return false; if (!profileId) return false;
try{ try {
const _id = DB.ObjectID(profileId); const _id = DB.ObjectID(profileId);
let r = await DB.profileCols.findOne({ _id }).catch((err) => { let r = await DB.profileCols.findOne({ _id }).catch((err) => {
console.log(err); console.log(err);
@@ -48,7 +48,7 @@ userDB = (DB) => {
}); });
if (r) userProfileCache[profileId] = r; if (r) userProfileCache[profileId] = r;
return r; return r;
}catch(_){ } catch (_) {
return {}; return {};
} }
} }
@@ -56,16 +56,16 @@ userDB = (DB) => {
DB.getPopularProfiles = async (limit = 10) => { DB.getPopularProfiles = async (limit = 10) => {
return DB.profileCols.aggregate([ 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) => { ]).limit(limit).toArray().catch((err) => {
console.log(err); console.log(err);
@@ -76,16 +76,16 @@ userDB = (DB) => {
DB.getPopularGroups = async (limit = 10) => { DB.getPopularGroups = async (limit = 10) => {
return DB.profileCols.aggregate([ 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) => { ]).limit(limit).toArray().catch((err) => {
console.log(err); console.log(err);
@@ -95,19 +95,19 @@ userDB = (DB) => {
DB.getFriendsFriends = async (profileId, limit = 10) => { DB.getFriendsFriends = async (profileId, limit = 10) => {
const profile = await DB.getProfile(profileId); const profile = await DB.getProfile(profileId);
if(!profile) return []; if (!profile) return [];
let ids = profile.following.map((id)=>DB.ObjectID(id)); let ids = profile.following.map((id) => DB.ObjectID(id));
let alreadyFollowingMap = {}; let alreadyFollowingMap = {};
alreadyFollowingMap[profileId] = 1; //skip that profile alreadyFollowingMap[profileId] = 1; //skip that profile
profile.following.forEach(id => { profile.following.forEach(id => {
if(!alreadyFollowingMap[id]) alreadyFollowingMap[id] = 1; 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 = {}; let friendsOfFriendsMap = {};
profiles.forEach(p => { profiles.forEach(p => {
p.following.forEach(followingId => { p.following.forEach(followingId => {
if(alreadyFollowingMap[followingId]) return 0; if (alreadyFollowingMap[followingId]) return 0;
if(!friendsOfFriendsMap[followingId]) friendsOfFriendsMap[followingId] = 0; if (!friendsOfFriendsMap[followingId]) friendsOfFriendsMap[followingId] = 0;
friendsOfFriendsMap[followingId] = friendsOfFriendsMap[followingId] + 1; friendsOfFriendsMap[followingId] = friendsOfFriendsMap[followingId] + 1;
}); });
}); });
@@ -124,24 +124,38 @@ userDB = (DB) => {
return DB.getProfile(profileId); 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) => { DB.searchProfile = async (queryStr) => {
let regEx = new RegExp(queryStr, 'i'); let regEx = new RegExp(queryStr, 'i');
let query = { let query = {
isGroup: false, isGroup: false,
isChat: {$ne: true}, isChat: { $ne: true },
$or: [ $or: [
{"profile.firstName": { {
"profile.firstName": {
$regex: regEx $regex: regEx
}}, }
{"profile.lastName": { },
{
"profile.lastName": {
$regex: regEx $regex: regEx
}}, }
{"profile.description": { },
{
"profile.description": {
$regex: regEx $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) .sort({ lastUpdate: -1 }).limit(20)
.toArray().catch((err) => { .toArray().catch((err) => {
console.log(err); console.log(err);
@@ -154,7 +168,7 @@ userDB = (DB) => {
const userid = DB.ObjectID(userId); const userid = DB.ObjectID(userId);
return await DB.profileCols.find({ userid }).toArray().catch((err) => { return await DB.profileCols.find({ userid }).toArray().catch((err) => {
console.log(err); console.log(err);
return false; return [];
}); });
} }
@@ -167,32 +181,32 @@ userDB = (DB) => {
return false; return false;
}); });
let index = 0; 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]; if (r[index]) userProfileCache[r[index]._id] = r[index];
return r[index]; return r[index];
} }
DB.followProfile = async (profileId, followProfileId)=>{ DB.followProfile = async (profileId, followProfileId) => {
const _id = DB.ObjectID(profileId); const _id = DB.ObjectID(profileId);
let update = { let update = {
$addToSet:{ $addToSet: {
following: followProfileId + '' //converts to str following: followProfileId + '' //converts to str
} }
} }
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
} }
DB.unfollowProfile = async (profileId, followProfileId)=>{ DB.unfollowProfile = async (profileId, followProfileId) => {
const _id = DB.ObjectID(profileId); const _id = DB.ObjectID(profileId);
let update = { let update = {
$pull:{ $pull: {
following: followProfileId + '' //converts to str following: followProfileId + '' //converts to str
} }
} }
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -200,7 +214,7 @@ userDB = (DB) => {
DB.getFollowingTheProfile = async (profileId) => { DB.getFollowingTheProfile = async (profileId) => {
//const profile_id = DB.ObjectID(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) => { .toArray().catch((err) => {
console.log(err); console.log(err);
return []; return [];
@@ -216,40 +230,40 @@ userDB = (DB) => {
DB.setData = async (profileid, key, value) => { DB.setData = async (profileid, key, value) => {
const _id = DB.ObjectID(profileid); const _id = DB.ObjectID(profileid);
let update = { let update = {
$set:{ $set: {
["data." + key]: value ["data." + key]: value
} }
} }
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
} }
DB.setProfileToken = (profileid, token)=>{ DB.setProfileToken = (profileid, token) => {
if(!token) return false; if (!token) return false;
const _id = DB.ObjectID(profileid); const _id = DB.ObjectID(profileid);
let update = { let update = {
$addToSet:{ $addToSet: {
token token
} }
} }
if (userProfileCache[profileid]) delete userProfileCache[profileid]; 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); console.log(err);
return false; return false;
}); });
} }
DB.setWebSubscription = (profileid, webSubscription)=>{ DB.setWebSubscription = (profileid, webSubscription) => {
const _id = DB.ObjectID(profileid); const _id = DB.ObjectID(profileid);
let update = { let update = {
$set:{ $set: {
webSubscription webSubscription
} }
} }
if (userProfileCache[profileid]) delete userProfileCache[profileid]; 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); console.log(err);
return false; return false;
}); });
@@ -258,7 +272,7 @@ userDB = (DB) => {
DB.addNotification = async (profileid, message, postid, commentIndx, actorid) => { DB.addNotification = async (profileid, message, postid, commentIndx, actorid) => {
const _id = DB.ObjectID(profileid); const _id = DB.ObjectID(profileid);
let update = { let update = {
$push:{ $push: {
notifications: { notifications: {
ts: new Date(), ts: new Date(),
body: message, body: message,
@@ -268,7 +282,7 @@ userDB = (DB) => {
} }
} }
} }
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -283,10 +297,10 @@ userDB = (DB) => {
DB.getGroups = async (excludePrivate = false) => { DB.getGroups = async (excludePrivate = false) => {
let query = { let query = {
isGroup: true, isGroup: true,
isCourse: {$ne: true}, isCourse: { $ne: true },
isChat: {$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) let r = await DB.profileCols.find(query).sort({ lastUpdate: -1 }).limit(10)
.toArray().catch((err) => { .toArray().catch((err) => {
console.log(err); console.log(err);
@@ -298,21 +312,21 @@ userDB = (DB) => {
DB.getFollowingGroups = async (profileid) => { DB.getFollowingGroups = async (profileid) => {
const profile = await DB.getProfile(profileid); const profile = await DB.getProfile(profileid);
let ids = []; let ids = [];
for(id in profile.following){ for (id in profile.following) {
try{ try {
let oId = DB.ObjectID(profile.following[id]); let oId = DB.ObjectID(profile.following[id]);
let checkProfile = await DB.getProfileCache(oId) let checkProfile = await DB.getProfileCache(oId)
if(checkProfile && checkProfile.isGroup && !checkProfile.isChat){ if (checkProfile && checkProfile.isGroup && !checkProfile.isChat) {
ids.push(oId) ids.push(oId)
} }
}catch{ } catch {
} }
} }
let query = { let query = {
isGroup: true, isGroup: true,
isCourse: {$ne: true}, isCourse: { $ne: true },
isChat: {$ne: true}, isChat: { $ne: true },
_id: { _id: {
$in: ids $in: ids
} }
@@ -329,23 +343,31 @@ userDB = (DB) => {
let regEx = new RegExp(queryStr, 'i'); let regEx = new RegExp(queryStr, 'i');
let query = queryStr ? { let query = queryStr ? {
isGroup: true, isGroup: true,
isChat: {$ne: true}, isChat: { $ne: true },
isCourse: coursesB, isCourse: coursesB,
$or: [ $or: [
{"profile.firstName": { {
"profile.firstName": {
$regex: regEx $regex: regEx
}}, }
{"profile.lastName": { },
{
"profile.lastName": {
$regex: regEx $regex: regEx
}}, }
{"profile.description": { },
{
"profile.description": {
$regex: regEx $regex: regEx
}}, }
{"data.author": { },
{
"data.author": {
$regex: regEx $regex: regEx
}} }
}
] ]
} : {isGroup: true, isChat: {$ne: true}, isCourse: coursesB}; } : { isGroup: true, isChat: { $ne: true }, isCourse: coursesB };
let r = await DB.profileCols.find(query) let r = await DB.profileCols.find(query)
.sort({ lastUpdate: -1 }).limit(20) .sort({ lastUpdate: -1 }).limit(20)
.toArray().catch((err) => { .toArray().catch((err) => {
@@ -357,13 +379,13 @@ userDB = (DB) => {
let privateGroupsCache = {}; let privateGroupsCache = {};
DB.isGroupPrivate = async (groupid) => { DB.isGroupPrivate = async (groupid) => {
if(userProfileCache[groupid]) return userProfileCache[groupid].isPrivate; if (userProfileCache[groupid]) return userProfileCache[groupid].isPrivate;
let g = await DB.getGroup(groupid); let g = await DB.getGroup(groupid);
return g ? g.isPrivate : false; return g ? g.isPrivate : false;
} }
DB.isGroupNewsOnly = async (groupid) => { DB.isGroupNewsOnly = async (groupid) => {
if(userProfileCache[groupid]) return userProfileCache[groupid].newsOnly; if (userProfileCache[groupid]) return userProfileCache[groupid].newsOnly;
let g = await DB.getGroup(groupid); let g = await DB.getGroup(groupid);
return g ? g.newsOnly : false; return g ? g.newsOnly : false;
} }
@@ -377,7 +399,7 @@ userDB = (DB) => {
DB.getGroup = async (groupid) => { DB.getGroup = async (groupid) => {
const _id = DB.ObjectID(groupid); const _id = DB.ObjectID(groupid);
//if(userProfileCache[groupid]) return userProfileCache[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); console.log(err);
return false; return false;
}); });
@@ -389,13 +411,13 @@ userDB = (DB) => {
const _id = DB.ObjectID(groupid); const _id = DB.ObjectID(groupid);
const subOrRequest = reqSubscription ? "pending." : "subscribed."; const subOrRequest = reqSubscription ? "pending." : "subscribed.";
let update = { let update = {
$set:{ $set: {
[subOrRequest + profileid]: new Date() [subOrRequest + profileid]: new Date()
} }
} }
if(!reqSubscription) DB.followProfile(profileid, groupid); if (!reqSubscription) DB.followProfile(profileid, groupid);
delete userProfileCache[groupid]; delete userProfileCache[groupid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -404,16 +426,16 @@ userDB = (DB) => {
DB.acceptGroupJoinReq = async (profileid, groupid) => { DB.acceptGroupJoinReq = async (profileid, groupid) => {
const _id = DB.ObjectID(groupid); const _id = DB.ObjectID(groupid);
let update = { let update = {
$set:{ $set: {
["subscribed." + profileid]: new Date() ["subscribed." + profileid]: new Date()
}, },
$unset:{ $unset: {
["pending." + profileid]: "" ["pending." + profileid]: ""
} }
} }
DB.followProfile(profileid, groupid); DB.followProfile(profileid, groupid);
delete userProfileCache[groupid]; delete userProfileCache[groupid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -422,12 +444,12 @@ userDB = (DB) => {
DB.rejectGroupJoinReq = async (profileid, groupid) => { DB.rejectGroupJoinReq = async (profileid, groupid) => {
const _id = DB.ObjectID(groupid); const _id = DB.ObjectID(groupid);
let update = { let update = {
$unset:{ $unset: {
["pending." + profileid]: "" ["pending." + profileid]: ""
} }
} }
delete userProfileCache[groupid]; delete userProfileCache[groupid];
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -436,12 +458,12 @@ userDB = (DB) => {
DB.unsubscribeToGroup = async (profileid, groupid) => { DB.unsubscribeToGroup = async (profileid, groupid) => {
const _id = DB.ObjectID(groupid); const _id = DB.ObjectID(groupid);
let update = { let update = {
$unset:{ $unset: {
["subscribed." + profileid]: "", ["subscribed." + profileid]: "",
} }
} }
DB.unfollowProfile(profileid, groupid) DB.unfollowProfile(profileid, groupid)
return DB.profileCols.updateOne({_id}, update).catch((err)=>{ return DB.profileCols.updateOne({ _id }, update).catch((err) => {
console.log(err); console.log(err);
return false; return false;
}); });
@@ -449,7 +471,7 @@ userDB = (DB) => {
//Courses //Courses
DB.getCourses = async () => { 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) .sort({ lastUpdate: -1 }).limit(20)
.toArray().catch((err) => { .toArray().catch((err) => {
console.log(err); console.log(err);

View File

@@ -1,15 +1,15 @@
class User { class User {
constructor(info){ 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.userid = info.userid;
this.profile = { this.profile = {
firstName: info.profile && info.profile.firstName || '', firstName: info.profile?.firstName || '',
lastName: info.profile && info.profile.lastName || '', lastName: info.profile?.lastName || '',
photo: info.profile && info.profile.photo || '', photo: info.profile?.photo || '',
location: info.profile && info.profile.location || 'USA', location: info.profile?.location || 'USA',
language: info.profile && info.profile.language || 'en', language: info.profile?.language || 'en',
status: info.profile && info.profile.status || '', status: info.profile?.status || '',
description: info.profile && info.profile.description || '', description: info.profile?.description || '',
}; };
this.data = info.data || {}; this.data = info.data || {};
this.username = info.username || ''; this.username = info.username || '';
@@ -23,8 +23,8 @@ class User {
this.isCourse = info.isCourse || false; this.isCourse = info.isCourse || false;
this.isPrivate = info.isPrivate || false; this.isPrivate = info.isPrivate || false;
this.isChat = info.isChat || false; this.isChat = info.isChat || false;
this.subscribed = info.subscribed || {}; //Subscribed user to groups this.subscribed = JSON.parse(JSON.stringify(info.subscribed || {})); //Subscribed user to groups
this.pending = info.pending || {}; //Private groups require authorization this.pending = JSON.parse(JSON.stringify(info.pending || {})); //Private groups require authorization
} }
toObj(){ toObj(){

296
index.js
View File

@@ -39,9 +39,119 @@ app.use(limiter);
// Authentication // Authentication
const { signup, login, logout, resetPassword } = require('./auth/authEmail.js'); const { signup, login, logout, resetPassword } = require('./auth/authEmail.js');
/**
* @swagger
* /signup:
* post:
* summary: Signs up a new user
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* format: email
* password:
* type: string
* format: password
* responses:
* 200:
* description: The user was successfully signed up.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* 400:
* description: Bad request.
*/
app.route('/signup').get(signup).post(signup); app.route('/signup').get(signup).post(signup);
/**
* @swagger
* /login:
* post:
* summary: Logs in a user
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* format: email
* password:
* type: string
* format: password
* responses:
* 200:
* description: The user was successfully logged in.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* 401:
* description: Invalid credentials.
*/
app.route('/login').get(login).post(login); app.route('/login').get(login).post(login);
/**
* @swagger
* /logout:
* get:
* summary: Logs out a user
* tags: [Auth]
* responses:
* 200:
* description: The user was successfully logged out.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
*/
app.get('/logout', logout); app.get('/logout', logout);
/**
* @swagger
* /resetPassword:
* post:
* summary: Resets a user's password
* tags: [Auth]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* format: email
* responses:
* 200:
* description: A password reset link has been sent to the user's email.
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* 400:
* description: Bad request.
*/
app.route('/resetPassword').post(resetPassword); app.route('/resetPassword').post(resetPassword);
// Routes // Routes
@@ -54,13 +164,49 @@ const sessionChecker = require('./middleware/sessionChecker');
// -- Private Routes // -- Private Routes
app.use('/user', sessionChecker, profileRoute); app.use('/user', sessionChecker, profileRoute);
app.use('/post', sessionChecker, postRoute); app.use('/post', sessionChecker, postRoute);
app.use('/payments', sessionChecker, paymentsRoute); app.use('/payments', paymentsRoute);
app.use('/bible', sessionChecker, bibleRoute); app.use('/bible', sessionChecker, bibleRoute);
app.use('/songs', sessionChecker, songsRoute); app.use('/songs', sessionChecker, songsRoute);
// -- Public Routes // -- Public Routes
const subsplashRoute = require('./routes/subsplash.js'); const subsplashRoute = require('./routes/subsplash.js');
app.use('/subsplash', subsplashRoute); 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 // Web Push Notifications
const webPush = require('web-push'); const webPush = require('web-push');
const publicVapidKey = process.env.PUBLIC_VAPID_KEY; const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
@@ -78,7 +224,31 @@ const DB = require("./mongoDB.js");
DB.getDB.then((DB) => { DB.getDB.then((DB) => {
console.log("Main logic: DB connected!"); 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) => { app.get('/', sessionChecker, async (req, res) => {
try { try {
const userInfo = req.userInfo; const userInfo = req.userInfo;
@@ -97,7 +267,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) => { app.get("/invite/:email", async (req, res) => {
try { try {
const email = req.params.email.trim().toLowerCase(); const email = req.params.email.trim().toLowerCase();
@@ -130,7 +331,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) => { app.post('/changeProfile', sessionChecker, async (req, res) => {
try { try {
const user_sid = getUserId(req); const user_sid = getUserId(req);
@@ -157,7 +393,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) => { app.post('/token/', sessionChecker, async (req, res) => {
try { try {
const profileid = getProfileId(req); const profileid = getProfileId(req);
@@ -179,7 +446,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) => { app.post('/subscribe', sessionChecker, async (req, res) => {
const subscription = req.body; const subscription = req.body;
res.status(201).json({}); res.status(201).json({});

View File

@@ -347,7 +347,7 @@ const Notifications = {
const bookedProfile = subscribed[index]; const bookedProfile = subscribed[index];
if (bookedProfile._id == senderProfile._id) return 0; if (bookedProfile._id == senderProfile._id) return 0;
const notifBody = `${senderProfile.profile.firstName} commented in a post you follow`; 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); DB.addNotification(bookedProfile._id, notifBody, post._id, post.comments.length - 1, senderProfile._id);
yourBookmarkedPostGotACommentTemplate(post, userEmail, postProfile, senderProfile, bookedProfile, message); 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 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`; 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); DB.addNotification(post.profileid, notifBody, postId, post.comments.length - 1, senderProfile._id);
return youGotANewPostCommentTemplate(post, userEmail, postProfile, senderProfile, message); return youGotANewPostCommentTemplate(post, userEmail, postProfile, senderProfile, message);
}, },
@@ -376,7 +376,7 @@ const Notifications = {
subscribed.forEach((bookedProfile) => { subscribed.forEach((bookedProfile) => {
if (bookedProfile._id == senderProfile._id) return 0; if (bookedProfile._id == senderProfile._id) return 0;
const notifBody = `${senderProfile.profile.firstName} liked a post you follow`; 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); 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 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`; 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); DB.addNotification(post.profileid, notifBody, postId, null, senderProfile._id);
return 0; return 0;
}, },
@@ -411,11 +411,11 @@ const Notifications = {
if (userProfile._id == senderProfile._id) return 0; //avoid sending self notifications if (userProfile._id == senderProfile._id) return 0; //avoid sending self notifications
if(groupProfile._id == senderProfile._id){ if(groupProfile._id == senderProfile._id){
const notifBody = `${groupProfile.profile.firstName} ${groupProfile.profile.lastName} has a new post!`; 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); 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}`; 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); DB.addNotification(userProfile._id, notifBody, post._id, null, senderProfile._id);
// Disabling email notifications for now, until settings are implemented // Disabling email notifications for now, until settings are implemented
// yourGroupGotANewPostTemplate(groupProfile, userEmail, userProfile, senderProfile, message); // yourGroupGotANewPostTemplate(groupProfile, userEmail, userProfile, senderProfile, message);
@@ -437,7 +437,7 @@ const Notifications = {
return this.broadcastNews(post, emails); return this.broadcastNews(post, emails);
} }
const notifBody = `${senderProfile.profile.firstName} post in your profile`; 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); // sendWebNotification(profile.webSubscription, notifBody, message);
DB.addNotification(toProfileId, notifBody, post._id, null, senderProfile._id); DB.addNotification(toProfileId, notifBody, post._id, null, senderProfile._id);
return youGotANewPostTemplate(profile, user.username, senderProfile, message); return youGotANewPostTemplate(profile, user.username, senderProfile, message);
@@ -461,7 +461,7 @@ const Notifications = {
const userProfile = subscribed_profiles[index]; const userProfile = subscribed_profiles[index];
if (userProfile._id == whoPostedId._id) return 0; if (userProfile._id == whoPostedId._id) return 0;
const notifBody = `${profile.profile.firstName} posted: ${message.substring(0, 50)}...`; 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); //sendWebNotification(userProfile.webSubscription, notifBody, message);
DB.addNotification(userProfile._id, notifBody, post._id, null, profile._id); 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", "posthog-node": "^4.4.1",
"socket.io": "^4.6.1", "socket.io": "^4.6.1",
"stripe": "^8.178.0", "stripe": "^8.178.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"web-push": "^3.4.5" "web-push": "^3.4.5"
}, },
"devDependencies": { "devDependencies": {
@@ -34,6 +36,50 @@
"supertest": "^7.0.0" "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": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "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" "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": { "node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
@@ -167,6 +219,13 @@
"node": ">=14" "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": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@@ -185,6 +244,12 @@
"@types/node": "*" "@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": { "node_modules/@types/node": {
"version": "16.10.2", "version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
@@ -305,7 +370,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/array-flatten": { "node_modules/array-flatten": {
@@ -516,6 +580,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/camelcase": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -757,6 +827,15 @@
"node": ">= 0.8" "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": { "node_modules/component-emitter": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -983,6 +1062,18 @@
"node": ">=0.3.1" "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": { "node_modules/dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -1221,6 +1312,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1884,7 +1984,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -1928,6 +2027,26 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -2421,6 +2540,13 @@
"wrappy": "1" "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": { "node_modules/optional-require": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", "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" "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": { "node_modules/tar": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
@@ -3326,6 +3529,15 @@
"node": ">= 0.4.0" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "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": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -3668,9 +3889,73 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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": { "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": { "@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "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": { "@mapbox/node-pre-gyp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
@@ -3759,6 +4049,11 @@
"dev": true, "dev": true,
"optional": 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": { "@socket.io/component-emitter": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@@ -3777,6 +4072,11 @@
"@types/node": "*" "@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": { "@types/node": {
"version": "16.10.2", "version": "16.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
@@ -3866,8 +4166,7 @@
"argparse": { "argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"dev": true
}, },
"array-flatten": { "array-flatten": {
"version": "1.1.1", "version": "1.1.1",
@@ -4032,6 +4331,11 @@
"get-intrinsic": "^1.2.6" "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": { "camelcase": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
@@ -4199,6 +4503,11 @@
"delayed-stream": "~1.0.0" "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": { "component-emitter": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -4361,6 +4670,14 @@
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true "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": { "dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -4525,6 +4842,11 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true "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": { "etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -4979,7 +5301,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": { "requires": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }
@@ -5012,6 +5333,21 @@
"p-locate": "^5.0.0" "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": { "log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -5346,6 +5682,12 @@
"wrappy": "1" "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": { "optional-require": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
@@ -5940,6 +6282,58 @@
"has-flag": "^4.0.0" "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": { "tar": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" "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": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "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": { "yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "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", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "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", "posthog-node": "^4.4.1",
"socket.io": "^4.6.1", "socket.io": "^4.6.1",
"stripe": "^8.178.0", "stripe": "^8.178.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"web-push": "^3.4.5" "web-push": "^3.4.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -13,11 +13,47 @@ const defaultBibleId = "592420522e16049f-01";
//getMedia('y42zyf3').then(console.log) //getMedia('y42zyf3').then(console.log)
DB.getDB.then((DB) => { 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) => { router.get("", async (req, res) => {
const bibles = await fetchAPI('bibles'); const bibles = await fetchAPI('bibles');
return res.json(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) => { router.get("/books", async (req, res) => {
const bibleId = req.query.bibleId || defaultBibleId; const bibleId = req.query.bibleId || defaultBibleId;
const bibles = await fetchAPI('bibles/' + bibleId +"/books"); const bibles = await fetchAPI('bibles/' + bibleId +"/books");
@@ -30,12 +66,56 @@ DB.getDB.then((DB) => {
return res.json(bibles); 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) => { router.get("/books/:bookId", async (req, res) => {
const bookId = req.params.bookId; const bookId = req.params.bookId;
const bibles = await fetchAPI('bibles/' + bibleId +"/books/" + bookId); const bibles = await fetchAPI('bibles/' + bibleId +"/books/" + bookId);
return res.json(bibles); 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) => { router.get("/books/:bookId/chapters", async (req, res) => {
const bookId = req.params.bookId; const bookId = req.params.bookId;
const bibleId = req.query.bibleId || defaultBibleId; const bibleId = req.query.bibleId || defaultBibleId;
@@ -43,6 +123,28 @@ DB.getDB.then((DB) => {
return res.json(bibles); 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) => { router.get("/chapters/:chapterId", async (req, res) => {
const chapterId = req.params.chapterId; const chapterId = req.params.chapterId;
const bibleId = req.query.bibleId || defaultBibleId; const bibleId = req.query.bibleId || defaultBibleId;
@@ -50,6 +152,28 @@ DB.getDB.then((DB) => {
return res.json(bibles); 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) => { router.get("/chapters/:chapterId/verses", async (req, res) => {
const chapterId = req.params.chapterId; const chapterId = req.params.chapterId;
const bibleId = req.query.bibleId || defaultBibleId; const bibleId = req.query.bibleId || defaultBibleId;
@@ -57,6 +181,33 @@ DB.getDB.then((DB) => {
return res.json(bibles); 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) => { router.get("/search", async (req, res) => {
const query = req.query.query; const query = req.query.query;
const limit = req.query.limit || 10; const limit = req.query.limit || 10;

View File

@@ -2,6 +2,7 @@ var express = require('express');
var router = express.Router(); var router = express.Router();
const DB = require("../mongoDB.js"); const DB = require("../mongoDB.js");
const mongo = require('mongodb');
//const Payments = require("../payments.js"); //const Payments = require("../payments.js");
const Stripe = require('stripe'); const Stripe = require('stripe');
const stripe = Stripe(process.env.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) => { let intent = async (req, res) => {
const userid = req.body.userid; const userid = req.body.userid;
const price = req.body.price || 500; 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 //Register in DB
const intent = { const intent = {
paymentIntent, paymentIntent,
@@ -67,12 +84,93 @@ DB.getDB.then((DB) => {
email: await DB.getUsernameByIdCache(userid), email: await DB.getUsernameByIdCache(userid),
price 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); 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); 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) => { router.post("/register", async (req, res) => {
const userid = req.body.userid; const userid = req.body.userid;
const result = req.body.result; const result = req.body.result;
@@ -84,7 +182,7 @@ DB.getDB.then((DB) => {
}; };
//console.log(payment); //console.log(payment);
const intent = await DB.getIntent(result.client_secret); const intent = await DB.getIntent(result.client_secret);
if(intent.description === "Subscription 1 Month"){ if (intent.description === "Subscription 1 Month") {
//update profile subscription status //update profile subscription status
const profileid = getProfileId(req); const profileid = getProfileId(req);
const isSubscriptor = await DB.isSubscriptor(profileid); const isSubscriptor = await DB.isSubscriptor(profileid);

View File

@@ -77,6 +77,83 @@ DB.getDB.then((DB) => {
return mergedPosts; 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) => { router.get("/organic", async (req, res) => {
const profileid = getProfileId(req); const profileid = getProfileId(req);
let organicPosts = await DB.getFeed(profileid); let organicPosts = await DB.getFeed(profileid);
@@ -86,6 +163,24 @@ DB.getDB.then((DB) => {
return res.json(posts); return res.json(posts);
}); });
/**
* @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) => { router.get("/", async (req, res) => {
const profileid = getProfileId(req); const profileid = getProfileId(req);
//Add non-organic posts //Add non-organic posts
@@ -95,6 +190,70 @@ DB.getDB.then((DB) => {
return res.json(posts); return res.json(posts);
}); });
/**
* @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) => { router.get("/usr/:id", async (req, res) => {
const profileId = req.params.id; const profileId = req.params.id;
const viewerProdileId = getProfileId(req); const viewerProdileId = getProfileId(req);
@@ -110,6 +269,35 @@ DB.getDB.then((DB) => {
return res.json(posts); 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) => { router.get("/usr/:id/images", async (req, res) => {
const profileid = req.params.id; const profileid = req.params.id;
const viewerProfileId = getProfileId(req); const viewerProfileId = getProfileId(req);
@@ -120,6 +308,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) => { router.get("/usr/:id/embedded", async (req, res) => {
const profileid = req.params.id; const profileid = req.params.id;
const viewerProfileId = getProfileId(req); const viewerProfileId = getProfileId(req);
@@ -130,6 +347,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) => { router.get("/usr/:id/media", async (req, res) => {
const profileid = req.params.id; const profileid = req.params.id;
const viewerProfileId = getProfileId(req); const viewerProfileId = getProfileId(req);
@@ -140,11 +386,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) => { router.get("/video/:id", async (req, res) => {
videoId = req.params.id; videoId = req.params.id;
return res.json([]); 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) => { router.post("/", async (req, res) => {
let post = { let post = {
profileid: getProfileId(req), profileid: getProfileId(req),
@@ -184,6 +468,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) => { router.post("/react", async (req, res) => {
let profileid = getProfileId(req); let profileid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -198,6 +503,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) => { router.post("/unreact", async (req, res) => {
let profileid = getProfileId(req); let profileid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -207,6 +533,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) => { router.post("/bookmark", async (req, res) => {
let profileid = getProfileId(req); let profileid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -216,6 +563,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) => { router.post("/unbookmark", async (req, res) => {
let profileid = getProfileId(req); let profileid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -225,6 +593,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) => { router.post("/comment/", async (req, res) => {
let profileid = getProfileId(req); let profileid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -244,6 +635,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) => { router.post("/comment/react", async (req, res) => {
let userid = getProfileId(req); let userid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -258,6 +673,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) => { router.post("/comment/unreact", async (req, res) => {
let profileid = getProfileId(req); let profileid = getProfileId(req);
let postid = req.body.postid; let postid = req.body.postid;
@@ -268,6 +707,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) => { router.get("/images", async (req, res) => {
const profileid = getProfileId(req); const profileid = getProfileId(req);
const posts = await DB.getMediaTagPostOfUser(profileid, profileid); const posts = await DB.getMediaTagPostOfUser(profileid, profileid);
@@ -277,6 +739,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) => { router.get("/embedded", async (req, res) => {
const profileid = getProfileId(req); const profileid = getProfileId(req);
const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@iframe:"); const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@iframe:");
@@ -286,6 +771,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) => { router.get("/media", async (req, res) => {
const profileid = getProfileId(req); const profileid = getProfileId(req);
const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@youtube:|@vimeo:|@hls:"); const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@youtube:|@vimeo:|@hls:");
@@ -295,6 +803,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) => { router.get("/course/recent", async (req, res) => {
const profileid = getProfileId(req); const profileid = getProfileId(req);
const profile = await DB.getProfileCache(profileid); const profile = await DB.getProfileCache(profileid);
@@ -342,6 +918,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) => { router.get("/:id", async (req, res) => {
const postId = req.params.id; const postId = req.params.id;
const post = await DB.getPost(postId); const post = await DB.getPost(postId);

View File

@@ -4,26 +4,112 @@ var router = express.Router()
const DB = require("../mongoDB.js"); const DB = require("../mongoDB.js");
const Profile = require("../def/profile.js"); const Profile = require("../def/profile.js");
const Notifications = require("./../notifications.js"); const Notifications = require("./../notifications.js");
const { getSessionId, getUserId, getProfileId } = require("./../utils/sessionUtils.js");
DB.getDB.then((DB)=>{ 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);
}
const profileBelongsToUser = async (profileid, userid) => { const profileBelongsToUser = async (profileid, userid) => {
const profile = await DB.getProfileCache(profileid); const profile = await DB.getProfileCache(profileid);
if(!profile) return false; if (!profile) return false;
return profile.userid == (userid + ''); 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) => { router.get("/mine", async (req, res) => {
let userid = req.cookies.user_sid; let userid = getUserId(req);
let profiles = await DB.getUserProfiles(userid); let profiles = await DB.getUserProfiles(userid);
return res.json({ return res.json({
status: "ok", 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("/") router.get("/new", async (req, res) => { //Deprecated please use route post("/")
let profile = { let profile = {
userid: getUserId(req), userid: getUserId(req),
... req.query.content ...req.query.content
}; };
let profileObj = new Profile(profile); let profileObj = new Profile(profile);
let r = await DB.newProfile(profileObj); let r = await DB.newProfile(profileObj);
return res.json({ return res.json({
status: "ok", 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) => { router.post("/", async (req, res) => {
let profile = { let profile = {
userid: getUserId(req), userid: getUserId(req),
... req.body.content ...req.body.content
}; };
try {
let profileObj = new Profile(profile); let profileObj = new Profile(profile);
let r = await DB.newProfile(profileObj); let r = await DB.newProfile(profileObj);
return res.json({ return res.json({
status: "ok", status: "ok",
... profileObj.toObj() ...profileObj.toObj()
}); });
}); } catch (error) {
console.error("Error creating profile", error);
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);
return res.json({ 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) => { router.get("/invite/:email", async (req, res) => {
const userid = getUserId(req); const userid = getUserId(req);
const email = req.params.email; const email = req.params.email;
//validate 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); 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); let isUserAlreadyRegistered = await DB.getUser(email);
if(isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({status: "This user is already registered"}); if (isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({ status: "This user is already registered" });
return res.json({status: "ok", ... r}); 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) => { router.get("/groups", async (req, res) => {
let groups = await DB.getGroups(); let groups = await DB.getGroups();
return res.json({ 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) => { router.get("/groups/following", async (req, res) => {
const profileId = getProfileId(req); const profileId = getProfileId(req);
let groups = await DB.getFollowingGroups(profileId); 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) => { router.post("/groups", async (req, res) => {
let profile = { let profile = {
userid: getUserId(req), userid: getUserId(req),
isGroup: true, isGroup: true,
... req.body ...req.body
}; };
let profileObj = new Profile(profile); let profileObj = new Profile(profile);
DB.newProfile(profileObj) DB.newProfile(profileObj)
return res.json({ return res.json({
status: "ok", 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) => { router.get("/courses", async (req, res) => {
let groups = await DB.getCourses(); let groups = await DB.getCourses();
return res.json({ 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) => { router.post("/groups/accept", async (req, res) => {
//This function should be called to accept the join request //This function should be called to accept the join request
//of an user that attempt to join a private group. //of an user that attempt to join a private group.
const groupid = getProfileId(req); //It needs to have this profile context const groupid = getProfileId(req); //It needs to have this profile context
const groupidBody = req.body.groupid ? DB.ObjectID(req.body.groupid) : undefined; 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({ return res.json({
status: "Only group owner can accept new subscribers" 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) => { router.post("/groups/reject", async (req, res) => {
//This function should be called to reject the join request //This function should be called to reject the join request
//of an user that attempt to join a private group. //of an user that attempt to join a private group.
const groupid = getProfileId(req); //It needs to have this profile context const groupid = getProfileId(req); //It needs to have this profile context
const groupidBody = req.body.groupid ? DB.ObjectID(req.body.groupid) : undefined; 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({ return res.json({
status: "Only group owner can reject new subscribers" 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) => { router.get("/groups/search", async (req, res) => {
let query = req.query.query; let query = req.query.query;
let coursesB = req.query.courses ? true : false; 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) => { router.get("/groups/:id", async (req, res) => {
const groupid = req.params.id; const groupid = req.params.id;
let groups = await DB.getGroup(groupid); 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) => { router.get("/groups/:id/subscribe", async (req, res) => {
const groupid = req.params.id; const groupid = req.params.id;
const profileid = getProfileId(req); const profileid = getProfileId(req);
const isPrivate = await DB.isGroupPrivate(groupid); const isPrivate = await DB.isGroupPrivate(groupid);
DB.subscribeToGroup(profileid, groupid, isPrivate); DB.subscribeToGroup(profileid, groupid, isPrivate);
//Add notification to group owner //Add notification to group owner
if(isPrivate) Notifications.yourGroupHasARequest(profileid, groupid) if (isPrivate) Notifications.yourGroupHasARequest(profileid, groupid)
return res.json({ return res.json({
status: "ok" 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) => { router.get("/groups/:id/unsubscribe", async (req, res) => {
const groupid = req.params.id; const groupid = req.params.id;
const profileid = getProfileId(req); 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) => { router.get("/search", async (req, res) => {
let query = req.query.query; let query = req.query.query;
let profiles = await DB.searchProfile(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) => { router.post("/setData", (req, res) => {
const key = req.body.key; const key = req.body.key;
const value = req.body.value; 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) => { router.post("/myProfile", async (req, res) => {
let profile = { let profile = {
userid: getUserId(req), userid: getUserId(req),
@@ -239,19 +772,66 @@ 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) => { router.get("/:id", async (req, res) => {
let profileId = req.params.id; let profileId = req.params.id;
let profile = await DB.getProfile(profileId); let profile = await DB.getProfile(profileId);
return res.json({ return res.json({
status: "ok", status: "ok",
... profile ...profile
}); });
}); });
/**
* @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) => { router.delete("/:id", async (req, res) => {
const profileId = req.params.id; const profileId = req.params.id;
const userid = getUserId(req); const userid = getUserId(req);
if(!await profileBelongsToUser(profileId, userid)) if (!await profileBelongsToUser(profileId, userid))
return res.json({ return res.json({
status: "This profile is not yours." status: "This profile is not yours."
}); });
@@ -261,6 +841,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) => { router.get("/:id/follow", async (req, res) => {
let followProfileId = req.params.id; let followProfileId = req.params.id;
const profileid = getProfileId(req); const profileid = getProfileId(req);
@@ -271,6 +876,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) => { router.get("/:id/unfollow", async (req, res) => {
let followProfileId = req.params.id; let followProfileId = req.params.id;
const profileid = getProfileId(req); 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); 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) => { router.get("/", async (req, res) => {
let profileId = req.params.id; let profileId = req.params.id;
let songs = await DB.getSongs(); 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) => { router.post("/", async (req, res) => {
let post = { let post = {
userid: getUserId(req), 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) => { router.get("/:id", async (req, res) => {
let profileId = req.params.id; let profileId = req.params.id;
let profile = await DB.getProfile(profileId); let profile = await DB.getProfile(profileId);
@@ -53,6 +108,24 @@ DB.getDB.then((DB)=>{
return true; 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) => { router.delete("/:id", async (req, res) => {
const userid = getUserId(req); const userid = getUserId(req);
const songId = req.params.id; 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) => { router.post("/:id", async (req, res) => {
const userid = getUserId(req); const userid = getUserId(req);
const songId = req.params.id; const songId = req.params.id;

View File

@@ -67,12 +67,51 @@ const getMedia = async (eventId) => {
}; };
//getMedia('y42zyf3').then(console.log) //getMedia('y42zyf3').then(console.log)
/**
* @swagger
* tags:
* name: Subsplash
* description: Subsplash API integration
*/
DB.getDB.then((DB) => { 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) => { router.get("/events/:calendarId", async (req, res) => {
const events = await getEvents(req.params.calendarId) const events = await getEvents(req.params.calendarId)
return res.json(events); 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) => { router.get("/media/:seriesId", async (req, res) => {
const events = await getMedia(req.params.seriesId) const events = await getMedia(req.params.seriesId)
return res.json(events); return res.json(events);

View File

@@ -1,14 +1,43 @@
const { ObjectId } = require("mongodb");
const isValidObjectId = (id) => ObjectId.isValid(id);
// Utilities // Utilities
const getSessionId = function (req) { const getSessionId = function (req) {
const session_id = req.cookies.session_id || req.query.session_id || req.body.session_id; 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; return session_id;
} }
const getUserId = function (req) { const getUserId = function (req) {
const user_sid = req.cookies.user_sid || req.query.user_sid || req.body.user_sid; 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; return user_sid;
} }
const getProfileId = function (req) { const getProfileId = function (req) {
const profile_id = req.cookies.profile_id || req.query.profile_id || req.body.profile_id; 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; return profile_id;
} }