Compare commits
17 Commits
7868df1abc
...
codex/pass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3a782a360 | ||
|
|
19d805d322 | ||
|
|
469962d03c | ||
|
|
c6d9dfd3c1 | ||
|
|
0baf237548 | ||
|
|
ea864b27d4 | ||
|
|
c136d25974 | ||
|
|
0b36db9b33 | ||
|
|
e4bac717f9 | ||
|
|
d21736d52c | ||
|
|
148ed696b2 | ||
|
|
0a48327e93 | ||
|
|
100b0c2a8f | ||
|
|
64ca9df639 | ||
|
|
46a2fc5c2b | ||
|
|
56cb8b4caa | ||
|
|
606db78529 |
139
README.md
139
README.md
@@ -2,8 +2,141 @@
|
||||
|
||||
This is the code for the backend of the EMI website.
|
||||
|
||||
## Getting Started
|
||||
|
||||
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
What things you need to install the software and how to install them:
|
||||
|
||||
```
|
||||
node.js
|
||||
npm
|
||||
mongodb
|
||||
```
|
||||
|
||||
### Installing
|
||||
|
||||
A step by step series of examples that tell you how to get a development env running:
|
||||
|
||||
1. Clone the repo
|
||||
2. Install NPM packages
|
||||
```
|
||||
npm install
|
||||
```
|
||||
3. Create a `.env` file with the necessary environment variables (`PORT`, `MONGO_URL`, `STRIPE`, etc.)
|
||||
4. Run the server
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
|
||||
Once the server is running, you can access the interactive API documentation powered by Swagger UI at:
|
||||
|
||||
`http://localhost:3000/api-docs`
|
||||
|
||||
This page allows you to view all available endpoints, their parameters, and test them directly from your browser.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The API is divided into several sections based on functionality. Most routes under `/user`, `/post`, `/bible`, and `/songs` require authentication via a session cookie.
|
||||
|
||||
### Authentication
|
||||
|
||||
- `POST /signup`: Creates a new user account.
|
||||
- `POST /login`: Logs in a user and creates a session.
|
||||
- `GET /logout`: Logs out the current user.
|
||||
- `POST /resetPassword`: Sends a password reset link to the user's email.
|
||||
|
||||
### General
|
||||
|
||||
- `GET /`: Returns basic information about the logged-in user.
|
||||
- `GET /invite/:email`: Checks if an invitation exists for a given email.
|
||||
- `POST /changeProfile`: Changes the active profile for the logged-in user.
|
||||
- `POST /token`: Refreshes the push notification token for a profile.
|
||||
- `POST /subscribe`: Subscribes a profile to web push notifications.
|
||||
|
||||
### Profiles (`/user`)
|
||||
|
||||
- `GET /mine`: Get all profiles for the logged-in user.
|
||||
- `POST /`: Creates a new profile.
|
||||
- `POST /invite`: Invites a new user by email.
|
||||
- `GET /invite/:email`: Get invitation details for an email.
|
||||
- `GET /groups`: Get a list of all groups.
|
||||
- `GET /groups/following`: Get a list of groups the current profile is following.
|
||||
- `POST /groups`: Create a new group.
|
||||
- `GET /courses`: Get a list of all courses.
|
||||
- `POST /groups/accept`: Accept a request to join a private group.
|
||||
- `POST /groups/reject`: Reject a request to join a private group.
|
||||
- `GET /groups/search`: Search for groups.
|
||||
- `GET /groups/:id`: Get details for a specific group.
|
||||
- `GET /groups/:id/subscribe`: Subscribe to a group.
|
||||
- `GET /groups/:id/unsubscribe`: Unsubscribe from a group.
|
||||
- `GET /search`: Search for profiles.
|
||||
- `POST /setData`: Set custom data for a profile.
|
||||
- `POST /myProfile`: Update the current user's profile.
|
||||
- `GET /:id`: Get a specific profile by ID.
|
||||
- `DELETE /:id`: Delete a profile.
|
||||
- `GET /:id/follow`: Follow a profile.
|
||||
- `GET /:id/unfollow`: Unfollow a profile.
|
||||
|
||||
### Posts (`/post`)
|
||||
|
||||
- `GET /organic`: Get the organic feed for the current user.
|
||||
- `GET /`: Get the feed with promotional content.
|
||||
- `GET /tag/:tag`: Get posts with a specific tag.
|
||||
- `GET /usr/:id`: Get posts from a specific user.
|
||||
- `GET /usr/:id/images`: Get all image posts from a user.
|
||||
- `GET /usr/:id/embedded`: Get all embedded posts from a user.
|
||||
- `GET /usr/:id/media`: Get all media posts from a user.
|
||||
- `POST /`: Create a new post.
|
||||
- `POST /react`: React to a post.
|
||||
- `POST /unreact`: Remove a reaction from a post.
|
||||
- `POST /bookmark`: Bookmark a post.
|
||||
- `POST /unbookmark`: Remove a bookmark from a post.
|
||||
- `POST /comment`: Add a comment to a post.
|
||||
- `POST /comment/react`: React to a comment.
|
||||
- `POST /comment/unreact`: Remove a reaction from a comment.
|
||||
- `GET /images`: Get all image posts for the current user.
|
||||
- `GET /embedded`: Get all embedded posts for the current user.
|
||||
- `GET /media`: Get all media posts for the current user.
|
||||
- `GET /course/recent`: Get recently watched media from courses.
|
||||
- `GET /:id`: Get a specific post by ID.
|
||||
- `DELETE /:id`: Delete a post.
|
||||
- `POST /:id`: Update a post.
|
||||
|
||||
### Payments (`/payments`)
|
||||
|
||||
- `POST /create-payment-intent`: Creates a Stripe Payment Intent.
|
||||
- `POST /intent`: (Alias for /create-payment-intent)
|
||||
- `POST /register`: Registers a payment after a successful Stripe transaction.
|
||||
|
||||
### Songs (`/songs`)
|
||||
|
||||
- `GET /`: Get all songs.
|
||||
- `POST /`: Create a new song.
|
||||
- `GET /:id`: Get a specific song by ID.
|
||||
- `DELETE /:id`: Delete a song.
|
||||
- `POST /:id`: Update a song.
|
||||
|
||||
### Bible (`/bible`)
|
||||
|
||||
- `GET /`: Get a list of available Bibles.
|
||||
- `GET /books`: Get the books of a Bible.
|
||||
- `GET /books/:bookId`: Get details for a specific book.
|
||||
- `GET /books/:bookId/chapters`: Get the chapters of a book.
|
||||
- `GET /chapters/:chapterId`: Get the content of a chapter.
|
||||
- `GET /chapters/:chapterId/verses`: Get the verses of a chapter.
|
||||
- `GET /search`: Search the Bible.
|
||||
|
||||
### Subsplash (`/subsplash`)
|
||||
|
||||
- `GET /events/:calendarId`: Get events from a Subsplash calendar.
|
||||
- `GET /media/:seriesId`: Get media from a Subsplash media series.
|
||||
|
||||
### TODO
|
||||
|
||||
[ ] Define nodes schema
|
||||
[ ] Implement basic login/registration
|
||||
[ ]
|
||||
- [ ] Define nodes schema
|
||||
- [ ] Implement basic login/registration
|
||||
@@ -9,15 +9,48 @@ const Notifications = require("../notifications");
|
||||
// Object Definitions
|
||||
const Post = require("../def/post.js")
|
||||
const Profile = require("../def/profile.js");
|
||||
const DUMMY_BCRYPT_HASH = '$2b$10$2zQfAaxK0cN13N7V2Q5hAOL3wxY5E9OQj1YxDCEV4VpWw2X2gYd6C';
|
||||
const PASSWORD_TOKEN_TTL_MINUTES = parseInt(process.env.PASSWORD_TOKEN_TTL_MINUTES || '20', 10);
|
||||
const PASSWORD_TOKEN_PATH = process.env.PASSWORD_TOKEN_PATH || '/token-login';
|
||||
const FRONTEND_URL = (process.env.FRONTEND_URL || 'https://social.emmint.com').replace(/\/+$/, '');
|
||||
|
||||
const createPasswordTokenHash = (rawToken) =>
|
||||
crypto.createHash('sha256').update(rawToken).digest('hex');
|
||||
|
||||
const createSessionFromUser = async ({ DB, user, req, res }) => {
|
||||
const sessionObj = await DB.newSession(user._id);
|
||||
res.cookie('user_sid', user._id, cookiesOptions);
|
||||
res.cookie('session_id', sessionObj.insertedId, cookiesOptions);
|
||||
const latestUpdatedProfile = await DB.latestProfile(user._id);
|
||||
if (latestUpdatedProfile && latestUpdatedProfile._id) {
|
||||
res.cookie('profile_id', latestUpdatedProfile._id, cookiesOptions);
|
||||
}
|
||||
client_logger.identify({
|
||||
distinctId: user._id,
|
||||
properties: {
|
||||
name: latestUpdatedProfile?.profile?.firstName || '',
|
||||
}
|
||||
});
|
||||
client_logger.capture({
|
||||
distinctId: user._id,
|
||||
event: 'server@' + req.method + '@' + req.originalUrl,
|
||||
});
|
||||
return {
|
||||
status: "ok",
|
||||
user_sid: user._id,
|
||||
session_id: sessionObj.insertedId,
|
||||
profile_id: latestUpdatedProfile?._id
|
||||
};
|
||||
};
|
||||
|
||||
// Function to Singup new users. An user is a combination of a user obj and a profile.
|
||||
// When new users are subscribed, they have a single profile, which is the personal one.
|
||||
// Other profiles can be link to that user, like groups or courses.
|
||||
const signup = async function (req, res) {
|
||||
const username = req.query.username || req.body.username;
|
||||
const password = req.query.password || req.body.password;
|
||||
const email = req.query.email || req.body.email;
|
||||
const profile = req.query.profile || req.body.profile;
|
||||
const username = (req.body.username || "").trim().toLowerCase();
|
||||
const password = req.body.password;
|
||||
const email = (req.body.email || "").trim().toLowerCase();
|
||||
const profile = req.body.profile;
|
||||
if (!username || !password || !email) return res.json({ status: "Incomplete information!" });
|
||||
// Check if the new user has an invitation.
|
||||
const DB = await MongoDB.getDB;
|
||||
@@ -34,12 +67,10 @@ const signup = async function (req, res) {
|
||||
}
|
||||
let isUserAlreadyRegistered = await DB.getUser(email);
|
||||
if (isUserAlreadyRegistered && isUserAlreadyRegistered._id) return res.json({ status: "This user is already registered" });
|
||||
// Hash password to be stored on the DB.
|
||||
// TODO: I think this is missing a Salt factor to improve security
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const newUserObject = await DB.newUser({
|
||||
username: username.toLowerCase(),
|
||||
email: email.toLowerCase(),
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword
|
||||
});
|
||||
// If newUserObject it's an error message, we check by looking toLowerCase function
|
||||
@@ -74,52 +105,28 @@ const login = async function (req, res) {
|
||||
// Check if user is already logged in and redirect to root if so.
|
||||
const session_id = getSessionId(req);
|
||||
const user_sid = getUserId(req);
|
||||
const DB = await MongoDB.getDB;
|
||||
if (session_id && user_sid) {
|
||||
const userInfo = await DB.checkSessionOnDB(session_id, user_sid);
|
||||
if (userInfo) return res.redirect('/');
|
||||
}
|
||||
const username = req.body.username || req.query.username;
|
||||
const password = req.body.password || req.query.password || "";
|
||||
const DB = await MongoDB.getDB;
|
||||
const invalidCredentials = () => res.status(401).json({ status: "Invalid credentials" });
|
||||
const username = (req.body.username || req.body.email || "").trim().toLowerCase();
|
||||
const password = req.body.password || "";
|
||||
if (!username || !password) return invalidCredentials();
|
||||
const user = await DB.getUser(username);
|
||||
|
||||
if (!user) {
|
||||
client_logger.capture({
|
||||
distinctId: 'app_level',
|
||||
event: 'server@' + req.method + '@' + req.originalUrl + '@userNotFound',
|
||||
properties: {
|
||||
username: username,
|
||||
}
|
||||
event: 'server@' + req.method + '@' + req.originalUrl + '@invalidCredentials',
|
||||
properties: { username },
|
||||
});
|
||||
return res.json({ status: "user not founded" });
|
||||
}
|
||||
// TODO: Also add salt parameter here.
|
||||
const isSamePassword = await bcrypt.compare(password, user.password);
|
||||
if (!isSamePassword) return res.json({ status: "incorrect password" });
|
||||
const isSamePassword = await bcrypt.compare(password, user?.password || DUMMY_BCRYPT_HASH);
|
||||
if (!user || !isSamePassword) return invalidCredentials();
|
||||
try {
|
||||
// Store a new session loging on DB, and use ID as session ID
|
||||
const sessionObj = await DB.newSession(user._id);
|
||||
// Create coockies with information for Auth
|
||||
res.cookie('user_sid', user._id, cookiesOptions);
|
||||
res.cookie('session_id', sessionObj.insertedId, cookiesOptions);
|
||||
// Chooses the most recent update profile as current active profile
|
||||
const latestUpdatedProfile = await DB.latestProfile(user._id);
|
||||
res.cookie('profile_id', latestUpdatedProfile._id, cookiesOptions);
|
||||
client_logger.identify({
|
||||
distinctId: user._id,
|
||||
properties: {
|
||||
name: latestUpdatedProfile.profile.firstName,
|
||||
}
|
||||
});
|
||||
client_logger.capture({
|
||||
distinctId: user._id,
|
||||
event: 'server@' + req.method + '@' + req.originalUrl,
|
||||
});
|
||||
return res.json({
|
||||
status: "ok",
|
||||
user_sid: user._id,
|
||||
session_id: sessionObj.insertedId,
|
||||
profile_id: latestUpdatedProfile._id
|
||||
});
|
||||
return res.json(await createSessionFromUser({ DB, user, req, res }));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
client_logger.capture({
|
||||
@@ -151,51 +158,43 @@ const logout = async function (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// Util function for generating new random password for users.
|
||||
function generatePassword(length = 12) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
|
||||
return Array.from(crypto.randomFillSync(new Uint8Array(length)))
|
||||
.map((x) => charset[x % charset.length])
|
||||
.join("");
|
||||
}
|
||||
|
||||
const resetPassword = async function (req, res) {
|
||||
const session_id = getSessionId(req);
|
||||
const user_sid = getUserId(req);
|
||||
const DB = await MongoDB.getDB;
|
||||
if (session_id && user_sid) {
|
||||
// Sadly reusing this endpoint to change password to legged in users.
|
||||
// TODO: Move change password logic to its own endpoint.
|
||||
const userInfo = await DB.checkSessionOnDB(session_id, user_sid);
|
||||
if (userInfo) {
|
||||
const password = req.body.password;
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
// TODO: Add salt to password here as well.
|
||||
DB.resetUserPassword(userInfo.username, hashedPassword);
|
||||
return res.json({
|
||||
|
||||
const genericResetResponse = {
|
||||
status: "ok",
|
||||
details: 'password changed!' // This should be an enum that syncs with clients.
|
||||
});
|
||||
}
|
||||
}
|
||||
details: "If the account exists, check your email for next steps"
|
||||
};
|
||||
// Logic for non-logged in users.
|
||||
const username = req.body.username;
|
||||
const username = (req.body.username || req.body.email || "").trim().toLowerCase();
|
||||
if (!username) return res.json(genericResetResponse);
|
||||
const user = await DB.getUser(username);
|
||||
if (!user) return res.json({ status: "user not founded" });
|
||||
const password = generatePassword();
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
// TODO: Add salt to password here as well.
|
||||
// TODO: We need to limit this to every 2 hours or something like this.
|
||||
// TODO: Move this template to the Notif file.
|
||||
DB.resetUserPassword(username, hashedPassword);
|
||||
Notifications.sendEmail(username, "Your new credentials",
|
||||
if (!user) {
|
||||
client_logger.capture({
|
||||
distinctId: 'app_level',
|
||||
event: 'server@' + req.method + '@' + req.originalUrl + '@resetRequestedUnknownUser',
|
||||
properties: { username }
|
||||
});
|
||||
return res.json(genericResetResponse);
|
||||
}
|
||||
const rawToken = crypto.randomBytes(32).toString('hex');
|
||||
const tokenHash = createPasswordTokenHash(rawToken);
|
||||
const expiresAt = new Date(Date.now() + PASSWORD_TOKEN_TTL_MINUTES * 60 * 1000);
|
||||
const tokenStored = await DB.createPasswordLoginToken(user._id, tokenHash, expiresAt);
|
||||
if (!tokenStored) {
|
||||
return res.json(genericResetResponse);
|
||||
}
|
||||
const loginUrl = `${FRONTEND_URL}${PASSWORD_TOKEN_PATH}?token=${rawToken}`;
|
||||
Notifications.sendEmail(username, "Your secure sign-in link",
|
||||
`
|
||||
<p>Hello,</p>
|
||||
<p> This is your new password: ${password}</p>
|
||||
<p><a href="https://social.emmint.com/">Log in</a></p>
|
||||
<p>Use this one-time sign-in link to access your account:</p>
|
||||
<p><a href="${loginUrl}">${loginUrl}</a></p>
|
||||
<p>This link expires in ${PASSWORD_TOKEN_TTL_MINUTES} minutes and can only be used once.</p>
|
||||
<p>If you did not request this, you can ignore this email.</p>
|
||||
<p>Blessings</p>
|
||||
<p>Emmanuel International Ministries</p>
|
||||
`)
|
||||
`);
|
||||
client_logger.capture({
|
||||
distinctId: user._id,
|
||||
event: 'server@' + req.method + '@' + req.originalUrl,
|
||||
@@ -203,10 +202,30 @@ const resetPassword = async function (req, res) {
|
||||
username: username,
|
||||
}
|
||||
});
|
||||
return res.json({
|
||||
status: "ok",
|
||||
details: 'Check your email for new password' // Enum of details?
|
||||
});
|
||||
return res.json(genericResetResponse);
|
||||
}
|
||||
|
||||
const loginWithPasswordToken = async function (req, res) {
|
||||
const DB = await MongoDB.getDB;
|
||||
const token = (req.body.token || "").trim();
|
||||
if (!token || token.length < 32) {
|
||||
return res.status(401).json({ status: "Invalid or expired token" });
|
||||
}
|
||||
const tokenHash = createPasswordTokenHash(token);
|
||||
const tokenDoc = await DB.consumePasswordLoginToken(tokenHash);
|
||||
if (!tokenDoc || !tokenDoc.userId) {
|
||||
return res.status(401).json({ status: "Invalid or expired token" });
|
||||
}
|
||||
const user = await DB.getUserById(tokenDoc.userId);
|
||||
if (!user || !user._id) {
|
||||
return res.status(401).json({ status: "Invalid or expired token" });
|
||||
}
|
||||
try {
|
||||
return res.json(await createSessionFromUser({ DB, user, req, res }));
|
||||
} catch (error) {
|
||||
console.error("Token login error", error);
|
||||
return res.status(500).json({ status: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,4 +234,5 @@ module.exports = {
|
||||
login,
|
||||
logout,
|
||||
resetPassword,
|
||||
loginWithPasswordToken,
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const forceSecureCookie = process.env.COOKIE_SECURE === "true";
|
||||
const secure = forceSecureCookie || isProduction;
|
||||
|
||||
const cookiesOptions = {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 90, // would expire after 30 days
|
||||
maxAge: 1000 * 60 * 60 * 24 * 90, // would expire after 90 days
|
||||
httpOnly: true, // The cookie only accessible by the web server
|
||||
sameSite: 'none', // This and secure are required for properly
|
||||
secure: true, // manage cockies in cros-domain
|
||||
sameSite: secure ? 'none' : 'lax',
|
||||
secure,
|
||||
};
|
||||
|
||||
module.exports = { cookiesOptions };
|
||||
@@ -1,6 +1,10 @@
|
||||
var corsOptions = {
|
||||
origin: [
|
||||
'http://localhost:8080',
|
||||
'http://localhost:8081',
|
||||
'http://127.0.0.1:3000',
|
||||
'http://127.0.0.1:8080',
|
||||
'http://127.0.0.1:8081',
|
||||
'http://localhost:3000',
|
||||
"https://social.emmint.com",
|
||||
"https://fellowship.emmint.com",
|
||||
|
||||
@@ -233,7 +233,7 @@ postDB = (DB)=>{
|
||||
if(!DB.ObjectID.isValid(profileId)) return [];
|
||||
const profile = await DB.getProfile(profileId);
|
||||
if(!profile) return [];
|
||||
query = {
|
||||
const query = {
|
||||
nonOrganicType: null // Exlcude news
|
||||
};
|
||||
return DB.postCols.find(query).sort({lastUpdated: -1}).limit(50).toArray().then(async (posts)=>{
|
||||
@@ -244,6 +244,25 @@ postDB = (DB)=>{
|
||||
});
|
||||
}
|
||||
|
||||
// For all post with tags const query = { content: { $regex: '#\\w+', $options: 'i' } };
|
||||
DB.getPostsByTag = async (tag, profileId, limit = 50) => {
|
||||
if(!DB.ObjectID.isValid(profileId)) return [];
|
||||
const profile = await DB.getProfile(profileId);
|
||||
if(!profile) return [];
|
||||
let query = {
|
||||
content: {
|
||||
"$regex": tag
|
||||
},
|
||||
nonOrganicType: null // Exlcude news
|
||||
};
|
||||
return DB.postCols.find(query).sort({lastUpdated: -1}).limit(limit).toArray().then(async (posts)=>{
|
||||
return await filterPrivateGroups(posts, profile);
|
||||
}).catch((err)=>{
|
||||
console.log(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
DB.getNews = async () => {
|
||||
let query = {
|
||||
nonOrganicType: 'News'
|
||||
|
||||
@@ -96,16 +96,18 @@ userDB = (DB) => {
|
||||
DB.getFriendsFriends = async (profileId, limit = 10) => {
|
||||
const profile = await DB.getProfile(profileId);
|
||||
if (!profile) return [];
|
||||
let ids = profile.following.map((id)=>DB.ObjectID(id));
|
||||
const following = Array.isArray(profile.following) ? profile.following : [];
|
||||
let ids = following.filter((id) => DB.ObjectID.isValid(id)).map((id) => DB.ObjectID(id));
|
||||
let alreadyFollowingMap = {};
|
||||
alreadyFollowingMap[profileId] = 1; //skip that profile
|
||||
profile.following.forEach(id => {
|
||||
following.forEach(id => {
|
||||
if (!alreadyFollowingMap[id]) alreadyFollowingMap[id] = 1;
|
||||
})
|
||||
return DB.profileCols.find({ _id: { $in: ids } }).project({ following: 1 }).limit(limit).toArray().then(profiles => {
|
||||
let friendsOfFriendsMap = {};
|
||||
profiles.forEach(p => {
|
||||
p.following.forEach(followingId => {
|
||||
const related = Array.isArray(p.following) ? p.following : [];
|
||||
related.forEach(followingId => {
|
||||
if (alreadyFollowingMap[followingId]) return 0;
|
||||
if (!friendsOfFriendsMap[followingId]) friendsOfFriendsMap[followingId] = 0;
|
||||
friendsOfFriendsMap[followingId] = friendsOfFriendsMap[followingId] + 1;
|
||||
@@ -124,21 +126,35 @@ userDB = (DB) => {
|
||||
return DB.getProfile(profileId);
|
||||
}
|
||||
|
||||
DB.getProfileCache = async (profileId) => {
|
||||
const cachedProfile = userProfileCache[profileId];
|
||||
if (cachedProfile?.isGroup === false) {
|
||||
return cachedProfile;
|
||||
}
|
||||
return await DB.getProfile(profileId);
|
||||
};
|
||||
|
||||
DB.searchProfile = async (queryStr) => {
|
||||
let regEx = new RegExp(queryStr, 'i');
|
||||
let query = {
|
||||
isGroup: false,
|
||||
isChat: { $ne: true },
|
||||
$or: [
|
||||
{"profile.firstName": {
|
||||
{
|
||||
"profile.firstName": {
|
||||
$regex: regEx
|
||||
}},
|
||||
{"profile.lastName": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile.lastName": {
|
||||
$regex: regEx
|
||||
}},
|
||||
{"profile.description": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile.description": {
|
||||
$regex: regEx
|
||||
}},
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
let r = await DB.profileCols.find(queryStr ? query : { isGroup: false, isChat: { $ne: true } })
|
||||
@@ -154,7 +170,7 @@ userDB = (DB) => {
|
||||
const userid = DB.ObjectID(userId);
|
||||
return await DB.profileCols.find({ userid }).toArray().catch((err) => {
|
||||
console.log(err);
|
||||
return false;
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -298,9 +314,10 @@ userDB = (DB) => {
|
||||
DB.getFollowingGroups = async (profileid) => {
|
||||
const profile = await DB.getProfile(profileid);
|
||||
let ids = [];
|
||||
for(id in profile.following){
|
||||
const following = Array.isArray(profile?.following) ? profile.following : [];
|
||||
for (id in following) {
|
||||
try {
|
||||
let oId = DB.ObjectID(profile.following[id]);
|
||||
let oId = DB.ObjectID(following[id]);
|
||||
let checkProfile = await DB.getProfileCache(oId)
|
||||
if (checkProfile && checkProfile.isGroup && !checkProfile.isChat) {
|
||||
ids.push(oId)
|
||||
@@ -332,18 +349,26 @@ userDB = (DB) => {
|
||||
isChat: { $ne: true },
|
||||
isCourse: coursesB,
|
||||
$or: [
|
||||
{"profile.firstName": {
|
||||
{
|
||||
"profile.firstName": {
|
||||
$regex: regEx
|
||||
}},
|
||||
{"profile.lastName": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile.lastName": {
|
||||
$regex: regEx
|
||||
}},
|
||||
{"profile.description": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile.description": {
|
||||
$regex: regEx
|
||||
}},
|
||||
{"data.author": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"data.author": {
|
||||
$regex: regEx
|
||||
}}
|
||||
}
|
||||
}
|
||||
]
|
||||
} : { isGroup: true, isChat: { $ne: true }, isCourse: coursesB };
|
||||
let r = await DB.profileCols.find(query)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
class User {
|
||||
constructor(info){
|
||||
if(!info || !info.userid) throw "Can not construct empty profile";
|
||||
if(!info || !info.userid) throw new Error("Cannot construct empty profile");
|
||||
this.userid = info.userid;
|
||||
this.profile = {
|
||||
firstName: info.profile && info.profile.firstName || '',
|
||||
lastName: info.profile && info.profile.lastName || '',
|
||||
photo: info.profile && info.profile.photo || '',
|
||||
location: info.profile && info.profile.location || 'USA',
|
||||
language: info.profile && info.profile.language || 'en',
|
||||
status: info.profile && info.profile.status || '',
|
||||
description: info.profile && info.profile.description || '',
|
||||
firstName: info.profile?.firstName || '',
|
||||
lastName: info.profile?.lastName || '',
|
||||
photo: info.profile?.photo || '',
|
||||
location: info.profile?.location || 'USA',
|
||||
language: info.profile?.language || 'en',
|
||||
status: info.profile?.status || '',
|
||||
description: info.profile?.description || '',
|
||||
};
|
||||
this.data = info.data || {};
|
||||
this.username = info.username || '';
|
||||
@@ -23,8 +23,8 @@ class User {
|
||||
this.isCourse = info.isCourse || false;
|
||||
this.isPrivate = info.isPrivate || false;
|
||||
this.isChat = info.isChat || false;
|
||||
this.subscribed = info.subscribed || {}; //Subscribed user to groups
|
||||
this.pending = info.pending || {}; //Private groups require authorization
|
||||
this.subscribed = JSON.parse(JSON.stringify(info.subscribed || {})); //Subscribed user to groups
|
||||
this.pending = JSON.parse(JSON.stringify(info.pending || {})); //Private groups require authorization
|
||||
}
|
||||
|
||||
toObj(){
|
||||
|
||||
329
index.js
329
index.js
@@ -9,6 +9,7 @@ require('dotenv').config();
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
app.set('trust proxy', true);
|
||||
// -- Accept request from other origins
|
||||
const cors = require('cors');
|
||||
const { corsOptions } = require('./config/corsOptions');
|
||||
@@ -34,15 +35,147 @@ const limiter = rateLimit({
|
||||
return ip.includes(":") ? ip.split(":")[0] : ip; // Remove port if present
|
||||
}
|
||||
});
|
||||
app.set('trust proxy', true);
|
||||
app.use(limiter);
|
||||
|
||||
// Authentication
|
||||
const { signup, login, logout, resetPassword } = require('./auth/authEmail.js');
|
||||
app.route('/signup').get(signup).post(signup);
|
||||
app.route('/login').get(login).post(login);
|
||||
const { signup, login, logout, resetPassword, loginWithPasswordToken } = require('./auth/authEmail.js');
|
||||
const { authRateLimiter } = require('./middleware/authRateLimiter');
|
||||
/**
|
||||
* @swagger
|
||||
* /signup:
|
||||
* post:
|
||||
* summary: Signs up a new user
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The user was successfully signed up.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: Bad request.
|
||||
*/
|
||||
app.post('/signup', signup);
|
||||
/**
|
||||
* @swagger
|
||||
* /login:
|
||||
* post:
|
||||
* summary: Logs in a user
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* password:
|
||||
* type: string
|
||||
* format: password
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The user was successfully logged in.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* 401:
|
||||
* description: Invalid credentials.
|
||||
*/
|
||||
app.post('/login', authRateLimiter('login'), login);
|
||||
/**
|
||||
* @swagger
|
||||
* /logout:
|
||||
* get:
|
||||
* summary: Logs out a user
|
||||
* tags: [Auth]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The user was successfully logged out.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
app.get('/logout', logout);
|
||||
app.route('/resetPassword').post(resetPassword);
|
||||
/**
|
||||
* @swagger
|
||||
* /resetPassword:
|
||||
* post:
|
||||
* summary: Sends a one-time sign-in link if the account exists
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* responses:
|
||||
* 200:
|
||||
* description: A password reset link has been sent to the user's email.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: Bad request.
|
||||
*/
|
||||
app.route('/resetPassword').post(authRateLimiter('reset'), resetPassword);
|
||||
/**
|
||||
* @swagger
|
||||
* /password/token-login:
|
||||
* post:
|
||||
* summary: Consumes a one-time password token and starts a session
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Logged in with one-time token
|
||||
* 401:
|
||||
* description: Invalid or expired token
|
||||
*/
|
||||
app.post('/password/token-login', authRateLimiter('token'), loginWithPasswordToken);
|
||||
|
||||
// Routes
|
||||
const profileRoute = require('./routes/profile.js');
|
||||
@@ -54,13 +187,49 @@ const sessionChecker = require('./middleware/sessionChecker');
|
||||
// -- Private Routes
|
||||
app.use('/user', sessionChecker, profileRoute);
|
||||
app.use('/post', sessionChecker, postRoute);
|
||||
app.use('/payments', sessionChecker, paymentsRoute);
|
||||
app.use('/payments', paymentsRoute);
|
||||
app.use('/bible', sessionChecker, bibleRoute);
|
||||
app.use('/songs', sessionChecker, songsRoute);
|
||||
// -- Public Routes
|
||||
const subsplashRoute = require('./routes/subsplash.js');
|
||||
app.use('/subsplash', subsplashRoute);
|
||||
|
||||
// Swagger API Docs
|
||||
const swaggerJSDoc = require('swagger-jsdoc');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
|
||||
const swaggerDefinition = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'EMI Backend API',
|
||||
version: '1.0.0',
|
||||
description: 'This is the REST API for the EMI Backend'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
description: 'Development server'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
cookieAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'cookie',
|
||||
name: 'user_sid'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const options = {
|
||||
swaggerDefinition,
|
||||
apis: ['./index.js', './routes/*.js']
|
||||
};
|
||||
|
||||
const swaggerSpec = swaggerJSDoc(options);
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
|
||||
// Web Push Notifications
|
||||
const webPush = require('web-push');
|
||||
const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
|
||||
@@ -78,7 +247,31 @@ const DB = require("./mongoDB.js");
|
||||
DB.getDB.then((DB) => {
|
||||
console.log("Main logic: DB connected!");
|
||||
|
||||
// route for Home-Page
|
||||
/**
|
||||
* @swagger
|
||||
* /:
|
||||
* get:
|
||||
* summary: Returns basic information about the logged-in user
|
||||
* tags: [General]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* userInfo:
|
||||
* type: object
|
||||
* profileInfo:
|
||||
* type: object
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
app.get('/', sessionChecker, async (req, res) => {
|
||||
try {
|
||||
const userInfo = req.userInfo;
|
||||
@@ -97,7 +290,38 @@ DB.getDB.then((DB) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Check for an invitation for an email
|
||||
/**
|
||||
* @swagger
|
||||
* /invite/{email}:
|
||||
* get:
|
||||
* summary: Checks if an invitation exists for a given email
|
||||
* tags: [General]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: email
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* format: email
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* invitation:
|
||||
* type: object
|
||||
* 400:
|
||||
* description: Provide a valid email
|
||||
* 404:
|
||||
* description: No invitation found for this email
|
||||
* 409:
|
||||
* description: This user is already registered
|
||||
*/
|
||||
app.get("/invite/:email", async (req, res) => {
|
||||
try {
|
||||
const email = req.params.email.trim().toLowerCase();
|
||||
@@ -130,7 +354,42 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
|
||||
|
||||
// Change the active profile for the user.
|
||||
/**
|
||||
* @swagger
|
||||
* /changeProfile:
|
||||
* post:
|
||||
* summary: Changes the active profile for the user
|
||||
* tags: [General]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* profileid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* profile:
|
||||
* type: object
|
||||
* 400:
|
||||
* description: Profile ID is required
|
||||
* 403:
|
||||
* description: Profile does not belong to the logged-in user
|
||||
* 404:
|
||||
* description: Profile does not exist
|
||||
*/
|
||||
app.post('/changeProfile', sessionChecker, async (req, res) => {
|
||||
try {
|
||||
const user_sid = getUserId(req);
|
||||
@@ -157,7 +416,38 @@ DB.getDB.then((DB) => {
|
||||
}
|
||||
});
|
||||
|
||||
// This is the endpoint to refresh the push notification token
|
||||
/**
|
||||
* @swagger
|
||||
* /token:
|
||||
* post:
|
||||
* summary: Refreshes the push notification token
|
||||
* tags: [General]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* token:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: Token is required
|
||||
* 500:
|
||||
* description: Failed to update token
|
||||
*/
|
||||
app.post('/token/', sessionChecker, async (req, res) => {
|
||||
try {
|
||||
const profileid = getProfileId(req);
|
||||
@@ -179,7 +469,24 @@ DB.getDB.then((DB) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Used for webpush notifications
|
||||
/**
|
||||
* @swagger
|
||||
* /subscribe:
|
||||
* post:
|
||||
* summary: Subscribes a user to webpush notifications
|
||||
* tags: [General]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created
|
||||
*/
|
||||
app.post('/subscribe', sessionChecker, async (req, res) => {
|
||||
const subscription = req.body;
|
||||
res.status(201).json({});
|
||||
|
||||
116
middleware/authRateLimiter.js
Normal file
116
middleware/authRateLimiter.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const crypto = require('crypto');
|
||||
const { client_logger } = require('../utils/analyticsLogger');
|
||||
|
||||
const AUTH_ATTEMPT_WINDOW_MS = Math.max(60 * 1000, parseInt(process.env.AUTH_ATTEMPT_WINDOW_MS || `${15 * 60 * 1000}`, 10));
|
||||
const AUTH_ATTEMPT_MAX = Math.max(1, parseInt(process.env.AUTH_ATTEMPT_MAX || '5', 10));
|
||||
const AUTH_BLOCK_BASE_MS = Math.max(30 * 1000, parseInt(process.env.AUTH_BLOCK_BASE_MS || `${5 * 60 * 1000}`, 10));
|
||||
const AUTH_BLOCK_MAX_MS = Math.max(AUTH_BLOCK_BASE_MS, parseInt(process.env.AUTH_BLOCK_MAX_MS || `${60 * 60 * 1000}`, 10));
|
||||
|
||||
const limiterStore = new Map();
|
||||
let lastPruneAt = 0;
|
||||
|
||||
const getClientIp = (req) => {
|
||||
const forwarded = req.headers['x-forwarded-for']?.split(',')[0]?.trim();
|
||||
const rawIp = forwarded || req.ip || req.connection?.remoteAddress || 'unknown';
|
||||
return rawIp.replace('::ffff:', '');
|
||||
};
|
||||
|
||||
const hashValue = (value) =>
|
||||
crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 16);
|
||||
|
||||
const getIdentity = (req, mode) => {
|
||||
if (mode === 'token') {
|
||||
const token = (req.body?.token || '').trim();
|
||||
return token ? `token:${hashValue(token)}` : 'token:anonymous';
|
||||
}
|
||||
const username = (req.body?.username || req.body?.email || '').trim().toLowerCase();
|
||||
return username ? `acct:${hashValue(username)}` : 'acct:anonymous';
|
||||
};
|
||||
|
||||
const getLimiterKey = (req, mode) => `${mode}:${getIdentity(req, mode)}:ip:${getClientIp(req)}`;
|
||||
|
||||
const getOrInitRecord = (key, now) => {
|
||||
const existing = limiterStore.get(key);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const record = {
|
||||
count: 0,
|
||||
windowStartedAt: now,
|
||||
blockedUntil: 0,
|
||||
blockLevel: 0,
|
||||
};
|
||||
limiterStore.set(key, record);
|
||||
return record;
|
||||
};
|
||||
|
||||
const computeBlockMs = (blockLevel) =>
|
||||
Math.min(AUTH_BLOCK_BASE_MS * (2 ** Math.max(0, blockLevel - 1)), AUTH_BLOCK_MAX_MS);
|
||||
|
||||
const authRateLimiter = (mode) => (req, res, next) => {
|
||||
const now = Date.now();
|
||||
if (now - lastPruneAt > 5 * 60 * 1000) {
|
||||
for (const [storeKey, storeValue] of limiterStore.entries()) {
|
||||
const isWindowExpired = now - storeValue.windowStartedAt > AUTH_ATTEMPT_WINDOW_MS;
|
||||
const isNotBlocked = storeValue.blockedUntil <= now;
|
||||
if (isWindowExpired && isNotBlocked) {
|
||||
limiterStore.delete(storeKey);
|
||||
}
|
||||
}
|
||||
lastPruneAt = now;
|
||||
}
|
||||
const key = getLimiterKey(req, mode);
|
||||
const record = getOrInitRecord(key, now);
|
||||
|
||||
if (now - record.windowStartedAt > AUTH_ATTEMPT_WINDOW_MS) {
|
||||
record.count = 0;
|
||||
record.windowStartedAt = now;
|
||||
}
|
||||
|
||||
if (record.blockedUntil > now) {
|
||||
const retryAfterSec = Math.ceil((record.blockedUntil - now) / 1000);
|
||||
res.set('Retry-After', retryAfterSec.toString());
|
||||
client_logger.capture({
|
||||
distinctId: 'app_level',
|
||||
event: 'security@auth@rate_limited',
|
||||
properties: {
|
||||
route: req.originalUrl,
|
||||
method: req.method,
|
||||
mode,
|
||||
keyHash: hashValue(key),
|
||||
retryAfterSec,
|
||||
blockLevel: record.blockLevel,
|
||||
}
|
||||
});
|
||||
return res.status(429).json({ status: 'Too many attempts. Please try again later.' });
|
||||
}
|
||||
|
||||
record.count += 1;
|
||||
if (record.count > AUTH_ATTEMPT_MAX) {
|
||||
record.blockLevel += 1;
|
||||
const blockMs = computeBlockMs(record.blockLevel);
|
||||
record.blockedUntil = now + blockMs;
|
||||
record.count = 0;
|
||||
record.windowStartedAt = now;
|
||||
res.set('Retry-After', Math.ceil(blockMs / 1000).toString());
|
||||
client_logger.capture({
|
||||
distinctId: 'app_level',
|
||||
event: 'security@auth@rate_limited',
|
||||
properties: {
|
||||
route: req.originalUrl,
|
||||
method: req.method,
|
||||
mode,
|
||||
keyHash: hashValue(key),
|
||||
retryAfterSec: Math.ceil(blockMs / 1000),
|
||||
blockLevel: record.blockLevel,
|
||||
}
|
||||
});
|
||||
return res.status(429).json({ status: 'Too many attempts. Please try again later.' });
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authRateLimiter,
|
||||
};
|
||||
@@ -2,19 +2,30 @@ const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils
|
||||
const { client_logger } = require('../utils/analyticsLogger');
|
||||
const { cookiesOptions } = require('../config/cookiesOptions');
|
||||
const MongoDB = require("../mongoDB.js");
|
||||
const { ObjectId } = require("mongodb");
|
||||
|
||||
const sessionChecker = async (req, res, next) => {
|
||||
try {
|
||||
const session_id = getSessionId(req);
|
||||
const user_sid = getUserId(req);
|
||||
let profile_id = getProfileId(req);
|
||||
|
||||
if (session_id && user_sid) {
|
||||
DB = await MongoDB.getDB;
|
||||
if (!session_id || !user_sid) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (!ObjectId.isValid(session_id) || !ObjectId.isValid(user_sid)) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
const DB = await MongoDB.getDB;
|
||||
const userInfo = await DB.checkSessionOnDB(session_id, user_sid);
|
||||
req.userInfo = userInfo;
|
||||
|
||||
if (!await DB.getProfileCache(profile_id)) {
|
||||
const latestProfile = await DB.latestProfile(user_sid);
|
||||
if (!latestProfile || !latestProfile._id) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
res.cookie('profile_id', latestProfile._id, cookiesOptions);
|
||||
profile_id = latestProfile._id;
|
||||
}
|
||||
@@ -23,14 +34,14 @@ const sessionChecker = async (req, res, next) => {
|
||||
|
||||
if (!userInfo) return res.redirect('/login');
|
||||
|
||||
// Log Request
|
||||
client_logger.capture({
|
||||
distinctId: user_sid,
|
||||
event: 'server@' + req.method + '@' + req.originalUrl,
|
||||
});
|
||||
|
||||
next();
|
||||
} else {
|
||||
} catch (error) {
|
||||
console.error("Session checker error", error);
|
||||
return res.redirect('/login');
|
||||
}
|
||||
};
|
||||
|
||||
60
mongoDB.js
60
mongoDB.js
@@ -11,19 +11,39 @@ const paymentDB = require("./dbTools/payments.js");
|
||||
const songsDB = require("./dbTools/songs.js");
|
||||
|
||||
console.log("Connecting to MongoDB...");
|
||||
const nodeMajorVersion = parseInt((process.versions.node || "0").split(".")[0], 10);
|
||||
if (nodeMajorVersion >= 22) {
|
||||
console.warn("Warning: mongodb@3.x is not fully tested on Node.js 22+. Prefer Node.js 20 LTS for local stability.");
|
||||
}
|
||||
|
||||
const mongoConnectOptions = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
serverSelectionTimeoutMS: 10000,
|
||||
connectTimeoutMS: 10000,
|
||||
socketTimeoutMS: 45000,
|
||||
keepAlive: true,
|
||||
};
|
||||
|
||||
const getDB = new Promise((resolve, reject) => {
|
||||
const DB = {ObjectID: mongo.ObjectID};
|
||||
MongoClient.connect(mongoUrl, function(err, db) {
|
||||
MongoClient.connect(mongoUrl, mongoConnectOptions, function(err, db) {
|
||||
if (err) return reject(err);
|
||||
|
||||
console.log("Connected to DB!");
|
||||
DB.db = db;
|
||||
DB.ObjectID = ObjectID;
|
||||
|
||||
DB.db.on("close", () => console.error("MongoDB connection closed"));
|
||||
DB.db.on("reconnect", () => console.log("MongoDB reconnected"));
|
||||
DB.db.on("error", (error) => console.error("MongoDB connection error", error));
|
||||
|
||||
DB.usersCol = db.db(DBName).collection("users");
|
||||
DB.tokensCol = db.db(DBName).collection("tokens");
|
||||
DB.invitationCol = db.db(DBName).collection("invitation");
|
||||
DB.passwordLoginTokensCol = db.db(DBName).collection("password_login_tokens");
|
||||
DB.passwordLoginTokensCol.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error);
|
||||
DB.passwordLoginTokensCol.createIndex({ tokenHash: 1 }, { unique: true }).catch(console.error);
|
||||
|
||||
DB.checkSessionOnDB = async (session_id, user_sid)=>{
|
||||
const temp_id = new mongo.ObjectID(session_id);
|
||||
@@ -31,7 +51,7 @@ const getDB = new Promise((resolve, reject) => {
|
||||
const doc = await DB.tokensCol.findOne({"_id":temp_id});
|
||||
if(doc && doc.uid == user_sid){
|
||||
const userMongoId = new mongo.ObjectID(user_sid);
|
||||
const userInfo = await DB.usersCol.findOne({"_id": userMongoId}, {fields: {password: 0}});
|
||||
const userInfo = await DB.usersCol.findOne({"_id": userMongoId}, {projection: {password: 0}});
|
||||
return userInfo;
|
||||
}
|
||||
return false;
|
||||
@@ -61,6 +81,42 @@ const getDB = new Promise((resolve, reject) => {
|
||||
return DB.usersCol.findOne({ _id });
|
||||
}
|
||||
|
||||
DB.createPasswordLoginToken = async (userId, tokenHash, expiresAt) => {
|
||||
const userObjectId = mongo.ObjectID.isValid(userId) ? new mongo.ObjectID(userId) : userId;
|
||||
const tokenDoc = {
|
||||
userId: userObjectId,
|
||||
tokenHash,
|
||||
createdAt: new Date(),
|
||||
expiresAt,
|
||||
usedAt: null,
|
||||
};
|
||||
return DB.passwordLoginTokensCol.insertOne(tokenDoc).catch((err) => {
|
||||
console.log(err);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
DB.consumePasswordLoginToken = async (tokenHash) => {
|
||||
const now = new Date();
|
||||
const result = await DB.passwordLoginTokensCol.findOneAndUpdate(
|
||||
{
|
||||
tokenHash,
|
||||
usedAt: null,
|
||||
expiresAt: { $gt: now }
|
||||
},
|
||||
{
|
||||
$set: { usedAt: now }
|
||||
},
|
||||
{
|
||||
returnOriginal: false
|
||||
}
|
||||
).catch((err) => {
|
||||
console.log(err);
|
||||
return false;
|
||||
});
|
||||
return result?.value || null;
|
||||
};
|
||||
|
||||
let usernamesCache = {}
|
||||
DB.getUsernameByIdCache = async (userid)=>{
|
||||
if(!userid) return {};
|
||||
|
||||
@@ -347,7 +347,7 @@ const Notifications = {
|
||||
const bookedProfile = subscribed[index];
|
||||
if (bookedProfile._id == senderProfile._id) return 0;
|
||||
const notifBody = `${senderProfile.profile.firstName} commented in a post you follow`;
|
||||
sendPushNotification(bookedProfile.token, notifBody, {});
|
||||
sendPushNotification(bookedProfile.token, notifBody, {post_id: post._id});
|
||||
DB.addNotification(bookedProfile._id, notifBody, post._id, post.comments.length - 1, senderProfile._id);
|
||||
yourBookmarkedPostGotACommentTemplate(post, userEmail, postProfile, senderProfile, bookedProfile, message);
|
||||
});
|
||||
@@ -363,7 +363,7 @@ const Notifications = {
|
||||
}
|
||||
if (postProfile.isCourse || senderProfile._id == postProfile._id) return 0; //Course owners do not need to receive notifs
|
||||
const notifBody = `${senderProfile.profile.firstName} commented in your post`;
|
||||
sendPushNotification(postProfile.token, notifBody, {});
|
||||
sendPushNotification(postProfile.token, notifBody, {post_id: postId, comment: message});
|
||||
DB.addNotification(post.profileid, notifBody, postId, post.comments.length - 1, senderProfile._id);
|
||||
return youGotANewPostCommentTemplate(post, userEmail, postProfile, senderProfile, message);
|
||||
},
|
||||
@@ -376,7 +376,7 @@ const Notifications = {
|
||||
subscribed.forEach((bookedProfile) => {
|
||||
if (bookedProfile._id == senderProfile._id) return 0;
|
||||
const notifBody = `${senderProfile.profile.firstName} liked a post you follow`;
|
||||
sendPushNotification(bookedProfile.token, notifBody, {});
|
||||
sendPushNotification(bookedProfile.token, notifBody, {post_id: post._id});
|
||||
DB.addNotification(bookedProfile._id, notifBody, post._id, null, senderProfile._id);
|
||||
});
|
||||
},
|
||||
@@ -391,7 +391,7 @@ const Notifications = {
|
||||
}
|
||||
if (postProfile.isCourse || senderProfile._id == postProfile._id) return 0; //Course owners do not need to receive notifs
|
||||
const notifBody = `${senderProfile.profile.firstName} liked your post`;
|
||||
sendPushNotification(postProfile.token, notifBody, {});
|
||||
sendPushNotification(postProfile.token, notifBody, {post_id: post._id});
|
||||
DB.addNotification(post.profileid, notifBody, postId, null, senderProfile._id);
|
||||
return 0;
|
||||
},
|
||||
@@ -411,11 +411,11 @@ const Notifications = {
|
||||
if (userProfile._id == senderProfile._id) return 0; //avoid sending self notifications
|
||||
if(groupProfile._id == senderProfile._id){
|
||||
const notifBody = `${groupProfile.profile.firstName} ${groupProfile.profile.lastName} has a new post!`;
|
||||
sendPushNotification(userProfile.token, notifBody, {});
|
||||
sendPushNotification(userProfile.token, notifBody, {post_id: post._id, profile_id: groupProfile._id});
|
||||
return DB.addNotification(userProfile._id, notifBody, post._id, null, senderProfile._id);
|
||||
}
|
||||
const notifBody = `${senderProfile.profile.firstName} post in the group ${groupProfile.profile.firstName} ${groupProfile.profile.lastName}`;
|
||||
sendPushNotification(userProfile.token, notifBody, {});
|
||||
sendPushNotification(userProfile.token, notifBody, {post_id: post._id, profile_id: groupProfile._id});
|
||||
DB.addNotification(userProfile._id, notifBody, post._id, null, senderProfile._id);
|
||||
// Disabling email notifications for now, until settings are implemented
|
||||
// yourGroupGotANewPostTemplate(groupProfile, userEmail, userProfile, senderProfile, message);
|
||||
@@ -437,7 +437,7 @@ const Notifications = {
|
||||
return this.broadcastNews(post, emails);
|
||||
}
|
||||
const notifBody = `${senderProfile.profile.firstName} post in your profile`;
|
||||
sendPushNotification(profile.token, notifBody, {});
|
||||
sendPushNotification(profile.token, notifBody, {post_id: post._id, profile_id: post.profileid});
|
||||
// sendWebNotification(profile.webSubscription, notifBody, message);
|
||||
DB.addNotification(toProfileId, notifBody, post._id, null, senderProfile._id);
|
||||
return youGotANewPostTemplate(profile, user.username, senderProfile, message);
|
||||
@@ -461,7 +461,7 @@ const Notifications = {
|
||||
const userProfile = subscribed_profiles[index];
|
||||
if (userProfile._id == whoPostedId._id) return 0;
|
||||
const notifBody = `${profile.profile.firstName} posted: ${message.substring(0, 50)}...`;
|
||||
sendPushNotification(userProfile.token, notifBody, {});
|
||||
sendPushNotification(userProfile.token, notifBody, {profile_id: whoPostedId});
|
||||
//sendWebNotification(userProfile.webSubscription, notifBody, message);
|
||||
DB.addNotification(userProfile._id, notifBody, post._id, null, profile._id);
|
||||
});
|
||||
|
||||
433
package-lock.json
generated
433
package-lock.json
generated
@@ -26,6 +26,8 @@
|
||||
"posthog-node": "^4.4.1",
|
||||
"socket.io": "^4.6.1",
|
||||
"stripe": "^8.178.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"web-push": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -34,6 +36,50 @@
|
||||
"supertest": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/openapi-schemas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-methods": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||
"@apidevtools/swagger-methods": "^3.0.2",
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"z-schema": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openapi-types": ">=7"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -137,6 +183,12 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
|
||||
@@ -167,6 +219,13 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
@@ -185,6 +244,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
|
||||
@@ -305,7 +370,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
@@ -516,6 +580,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
@@ -757,6 +827,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||
@@ -983,6 +1062,18 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -1221,6 +1312,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
@@ -1884,7 +1984,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -1928,6 +2027,26 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
@@ -2421,6 +2540,13 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/optional-require": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
|
||||
@@ -3251,6 +3377,83 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-jsdoc": {
|
||||
"version": "6.2.8",
|
||||
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "6.2.0",
|
||||
"doctrine": "3.0.0",
|
||||
"glob": "7.1.6",
|
||||
"lodash.mergewith": "^4.6.2",
|
||||
"swagger-parser": "^10.0.3",
|
||||
"yaml": "2.0.0-1"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-jsdoc/node_modules/glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-parser": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.27.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.0.tgz",
|
||||
"integrity": "sha512-tS6LRyBhY6yAqxrfsA9IYpGWPUJOri6sclySa7TdC7XQfGLvTwDY531KLgfQwHEtQsn+sT4JlUspbeQDBVGWig==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-express": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"swagger-ui-dist": ">=5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= v0.10.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
@@ -3326,6 +3529,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -3563,6 +3775,15 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.0.0-1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
@@ -3668,9 +3889,73 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/z-schema": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"validator": "^13.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"z-schema": "bin/z-schema"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"commander": "^9.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/z-schema/node_modules/commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||
"requires": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"@apidevtools/openapi-schemas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ=="
|
||||
},
|
||||
"@apidevtools/swagger-methods": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
|
||||
},
|
||||
"@apidevtools/swagger-parser": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||
"requires": {
|
||||
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||
"@apidevtools/swagger-methods": "^3.0.2",
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"z-schema": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -3736,6 +4021,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
|
||||
},
|
||||
"@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
|
||||
@@ -3759,6 +4049,11 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
@@ -3777,6 +4072,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
|
||||
@@ -3866,8 +4166,7 @@
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
@@ -4032,6 +4331,11 @@
|
||||
"get-intrinsic": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
@@ -4199,6 +4503,11 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q=="
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||
@@ -4361,6 +4670,14 @@
|
||||
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
|
||||
"dev": true
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||
"requires": {
|
||||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -4525,6 +4842,11 @@
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true
|
||||
},
|
||||
"esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
@@ -4979,7 +5301,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
@@ -5012,6 +5333,21 @@
|
||||
"p-locate": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
@@ -5346,6 +5682,12 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||
"peer": true
|
||||
},
|
||||
"optional-require": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
|
||||
@@ -5940,6 +6282,58 @@
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"swagger-jsdoc": {
|
||||
"version": "6.2.8",
|
||||
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||
"requires": {
|
||||
"commander": "6.2.0",
|
||||
"doctrine": "3.0.0",
|
||||
"glob": "7.1.6",
|
||||
"lodash.mergewith": "^4.6.2",
|
||||
"swagger-parser": "^10.0.3",
|
||||
"yaml": "2.0.0-1"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"swagger-parser": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||
"requires": {
|
||||
"@apidevtools/swagger-parser": "10.0.3"
|
||||
}
|
||||
},
|
||||
"swagger-ui-dist": {
|
||||
"version": "5.27.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.0.tgz",
|
||||
"integrity": "sha512-tS6LRyBhY6yAqxrfsA9IYpGWPUJOri6sclySa7TdC7XQfGLvTwDY531KLgfQwHEtQsn+sT4JlUspbeQDBVGWig==",
|
||||
"requires": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"swagger-ui-express": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||
"requires": {
|
||||
"swagger-ui-dist": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
@@ -5996,6 +6390,11 @@
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -6154,6 +6553,11 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yaml": {
|
||||
"version": "2.0.0-1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
@@ -6226,6 +6630,25 @@
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"z-schema": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||
"requires": {
|
||||
"commander": "^9.4.1",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"validator": "^13.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"posthog-node": "^4.4.1",
|
||||
"socket.io": "^4.6.1",
|
||||
"stripe": "^8.178.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"web-push": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
151
routes/bible.js
151
routes/bible.js
@@ -13,11 +13,47 @@ const defaultBibleId = "592420522e16049f-01";
|
||||
|
||||
//getMedia('y42zyf3').then(console.log)
|
||||
DB.getDB.then((DB) => {
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Bible
|
||||
* description: Bible API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible:
|
||||
* get:
|
||||
* summary: Get a list of available Bibles
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("", async (req, res) => {
|
||||
const bibles = await fetchAPI('bibles');
|
||||
return res.json(bibles);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible/books:
|
||||
* get:
|
||||
* summary: Get the books of a Bible
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: bibleId
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/books", async (req, res) => {
|
||||
const bibleId = req.query.bibleId || defaultBibleId;
|
||||
const bibles = await fetchAPI('bibles/' + bibleId +"/books");
|
||||
@@ -30,12 +66,56 @@ DB.getDB.then((DB) => {
|
||||
return res.json(bibles);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible/books/{bookId}:
|
||||
* get:
|
||||
* summary: Get details for a specific book
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: bookId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: bibleId
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/books/:bookId", async (req, res) => {
|
||||
const bookId = req.params.bookId;
|
||||
const bibles = await fetchAPI('bibles/' + bibleId +"/books/" + bookId);
|
||||
return res.json(bibles);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible/books/{bookId}/chapters:
|
||||
* get:
|
||||
* summary: Get the chapters of a book
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: bookId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: bibleId
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/books/:bookId/chapters", async (req, res) => {
|
||||
const bookId = req.params.bookId;
|
||||
const bibleId = req.query.bibleId || defaultBibleId;
|
||||
@@ -43,6 +123,28 @@ DB.getDB.then((DB) => {
|
||||
return res.json(bibles);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible/chapters/{chapterId}:
|
||||
* get:
|
||||
* summary: Get the content of a chapter
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: chapterId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: bibleId
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/chapters/:chapterId", async (req, res) => {
|
||||
const chapterId = req.params.chapterId;
|
||||
const bibleId = req.query.bibleId || defaultBibleId;
|
||||
@@ -50,6 +152,28 @@ DB.getDB.then((DB) => {
|
||||
return res.json(bibles);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible/chapters/{chapterId}/verses:
|
||||
* get:
|
||||
* summary: Get the verses of a chapter
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: chapterId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: bibleId
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/chapters/:chapterId/verses", async (req, res) => {
|
||||
const chapterId = req.params.chapterId;
|
||||
const bibleId = req.query.bibleId || defaultBibleId;
|
||||
@@ -57,6 +181,33 @@ DB.getDB.then((DB) => {
|
||||
return res.json(bibles);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bible/search:
|
||||
* get:
|
||||
* summary: Search the Bible
|
||||
* tags: [Bible]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* - in: query
|
||||
* name: bibleId
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/search", async (req, res) => {
|
||||
const query = req.query.query;
|
||||
const limit = req.query.limit || 10;
|
||||
|
||||
@@ -2,6 +2,7 @@ var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
const DB = require("../mongoDB.js");
|
||||
const mongo = require('mongodb');
|
||||
//const Payments = require("../payments.js");
|
||||
const Stripe = require('stripe');
|
||||
const stripe = Stripe(process.env.STRIPE);
|
||||
@@ -38,6 +39,13 @@ DB.getDB.then((DB) => {
|
||||
// });
|
||||
// });
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Payments
|
||||
* description: Payment processing
|
||||
*/
|
||||
|
||||
let intent = async (req, res) => {
|
||||
const userid = req.body.userid;
|
||||
const price = req.body.price || 500;
|
||||
@@ -52,6 +60,15 @@ DB.getDB.then((DB) => {
|
||||
],
|
||||
});
|
||||
|
||||
// check if user is email or userid
|
||||
const isUserId = mongo.ObjectId.isValid(userid);
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const isEmail = emailRegex.test(userid.trim().toLowerCase());
|
||||
console.log("isUserId: ", isUserId);
|
||||
console.log("isEmail: ", isEmail);
|
||||
console.log("userid: ", userid);
|
||||
|
||||
if (isUserId) {
|
||||
//Register in DB
|
||||
const intent = {
|
||||
paymentIntent,
|
||||
@@ -67,12 +84,93 @@ DB.getDB.then((DB) => {
|
||||
email: await DB.getUsernameByIdCache(userid),
|
||||
price
|
||||
});
|
||||
}
|
||||
if (isEmail) {
|
||||
//Register in DB
|
||||
return res.send({
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
email: userid,
|
||||
price
|
||||
});
|
||||
}
|
||||
return res.send({
|
||||
clientSecret: paymentIntent.client_secret,
|
||||
email: 'guess',
|
||||
price
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /payments/create-payment-intent:
|
||||
* post:
|
||||
* summary: Creates a Stripe Payment Intent
|
||||
* tags: [Payments]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* userid:
|
||||
* type: string
|
||||
* price:
|
||||
* type: number
|
||||
* description:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/create-payment-intent", intent);
|
||||
/**
|
||||
* @swagger
|
||||
* /payments/intent:
|
||||
* post:
|
||||
* summary: Creates a Stripe Payment Intent (Alias for /create-payment-intent)
|
||||
* tags: [Payments]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* userid:
|
||||
* type: string
|
||||
* price:
|
||||
* type: number
|
||||
* description:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/intent", intent);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /payments/register:
|
||||
* post:
|
||||
* summary: Registers a payment after a successful Stripe transaction
|
||||
* tags: [Payments]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* userid:
|
||||
* type: string
|
||||
* result:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/register", async (req, res) => {
|
||||
const userid = req.body.userid;
|
||||
const result = req.body.result;
|
||||
|
||||
621
routes/post.js
621
routes/post.js
@@ -8,7 +8,10 @@ const Notifications = require("./../notifications.js");
|
||||
DB.getDB.then((DB) => {
|
||||
|
||||
const getProfileId = (req) => {
|
||||
return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id);
|
||||
const rawProfileId = req.cookies.profile_id || req.query.profile_id || req.body.profile_id || req.profileInfo?._id;
|
||||
if (!rawProfileId) return null;
|
||||
if (!DB.ObjectID.isValid(rawProfileId)) return null;
|
||||
return DB.ObjectID(rawProfileId);
|
||||
};
|
||||
|
||||
const postBelongToProfile = (post, profileid) => {
|
||||
@@ -77,24 +80,193 @@ DB.getDB.then((DB) => {
|
||||
return mergedPosts;
|
||||
};
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Posts
|
||||
* description: Post management
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Post:
|
||||
* type: object
|
||||
* properties:
|
||||
* profileid:
|
||||
* type: string
|
||||
* description: The ID of the profile that created the post.
|
||||
* content:
|
||||
* type: string
|
||||
* description: The content of the post.
|
||||
* createdAt:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The timestamp when the post was created.
|
||||
* reactions:
|
||||
* type: object
|
||||
* description: An object containing reactions to the post, keyed by profile ID.
|
||||
* comments:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* description: An array of comments on the post.
|
||||
* bookmarks:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: An array of profile IDs that bookmarked the post.
|
||||
* nonOrganicType:
|
||||
* type: string
|
||||
* description: Type of non-organic content (e.g., "News", "PopularUsers", "PopularGroups").
|
||||
* contentHistory:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: A history of content edits.
|
||||
* toProfile:
|
||||
* type: string
|
||||
* description: The ID of the profile this post is directed to (e.g., a group).
|
||||
* lastUpdated:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: The timestamp when the post was last updated.
|
||||
* tags:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: An array of tags associated with the post.
|
||||
* chatSenderId:
|
||||
* type: string
|
||||
* description: The ID of the chat sender, if applicable.
|
||||
*
|
||||
* /post/organic:
|
||||
* get:
|
||||
* summary: Get the organic feed for the current user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/organic", async (req, res) => {
|
||||
try {
|
||||
const profileid = getProfileId(req);
|
||||
if (!profileid) return res.status(400).json([]);
|
||||
let organicPosts = await DB.getFeed(profileid);
|
||||
//Add non-organic posts
|
||||
const nonOrganicPosts = await generateNonOrganicPosts(req, profileid);
|
||||
const posts = mergePosts(organicPosts, nonOrganicPosts);
|
||||
const posts = mergePosts(organicPosts || [], nonOrganicPosts || []);
|
||||
return res.json(posts);
|
||||
} catch (error) {
|
||||
console.error("Error loading organic feed", error);
|
||||
return res.status(500).json([]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post:
|
||||
* get:
|
||||
* summary: Get the feed with promotional content
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const profileid = getProfileId(req);
|
||||
//Add non-organic posts
|
||||
if (!profileid) return res.status(400).json([]);
|
||||
const nonOrganicPosts = await generateNonOrganicPosts(req, profileid);
|
||||
let promotionalPosts = await DB.getPromotionalPosts(profileid);
|
||||
const posts = mergePosts(promotionalPosts, nonOrganicPosts);
|
||||
const posts = mergePosts(promotionalPosts || [], nonOrganicPosts || []);
|
||||
return res.json(posts);
|
||||
} catch (error) {
|
||||
console.error("Error loading feed", error);
|
||||
return res.status(500).json([]);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/tag/{tag}:
|
||||
* get:
|
||||
* summary: Get posts with a specific tag
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: tag
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
* 400:
|
||||
* description: Tag is required
|
||||
*/
|
||||
router.get("/tag/:tag", async (req, res) => {
|
||||
const profileid = getProfileId(req);
|
||||
const tag = req.query.tag || req.params.tag;
|
||||
if(!tag) {
|
||||
console.log("Tag query empty: ", tag);
|
||||
return res.json({
|
||||
status: "Tag is required",
|
||||
});
|
||||
}
|
||||
console.log("Tag query: ", tag);
|
||||
let posts = await DB.getPostsByTag('#' + tag, profileid);
|
||||
return res.json(posts);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/usr/{id}:
|
||||
* get:
|
||||
* summary: Get posts from a specific user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/usr/:id", async (req, res) => {
|
||||
const profileId = req.params.id;
|
||||
const viewerProdileId = getProfileId(req);
|
||||
@@ -110,6 +282,35 @@ DB.getDB.then((DB) => {
|
||||
return res.json(posts);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/usr/{id}/images:
|
||||
* get:
|
||||
* summary: Get all image posts from a user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* posts:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/usr/:id/images", async (req, res) => {
|
||||
const profileid = req.params.id;
|
||||
const viewerProfileId = getProfileId(req);
|
||||
@@ -120,6 +321,35 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/usr/{id}/embedded:
|
||||
* get:
|
||||
* summary: Get all embedded posts from a user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* posts:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/usr/:id/embedded", async (req, res) => {
|
||||
const profileid = req.params.id;
|
||||
const viewerProfileId = getProfileId(req);
|
||||
@@ -130,6 +360,35 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/usr/{id}/media:
|
||||
* get:
|
||||
* summary: Get all media posts from a user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* posts:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/usr/:id/media", async (req, res) => {
|
||||
const profileid = req.params.id;
|
||||
const viewerProfileId = getProfileId(req);
|
||||
@@ -140,11 +399,49 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/video/{id}:
|
||||
* get:
|
||||
* summary: Get video details by ID
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/video/:id", async (req, res) => {
|
||||
videoId = req.params.id;
|
||||
return res.json([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post:
|
||||
* post:
|
||||
* summary: Create a new post
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 403:
|
||||
* description: Not authorized to post to this group
|
||||
*/
|
||||
router.post("/", async (req, res) => {
|
||||
let post = {
|
||||
profileid: getProfileId(req),
|
||||
@@ -184,6 +481,27 @@ DB.getDB.then((DB) => {
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/react:
|
||||
* post:
|
||||
* summary: React to a post
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/react", async (req, res) => {
|
||||
let profileid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -198,6 +516,27 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/unreact:
|
||||
* post:
|
||||
* summary: Remove a reaction from a post
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/unreact", async (req, res) => {
|
||||
let profileid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -207,6 +546,27 @@ DB.getDB.then((DB) => {
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/bookmark:
|
||||
* post:
|
||||
* summary: Bookmark a post
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/bookmark", async (req, res) => {
|
||||
let profileid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -216,6 +576,27 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/unbookmark:
|
||||
* post:
|
||||
* summary: Remove a bookmark from a post
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/unbookmark", async (req, res) => {
|
||||
let profileid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -225,6 +606,29 @@ DB.getDB.then((DB) => {
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/comment:
|
||||
* post:
|
||||
* summary: Add a comment to a post
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* content:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/comment/", async (req, res) => {
|
||||
let profileid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -244,6 +648,30 @@ DB.getDB.then((DB) => {
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/comment/react:
|
||||
* post:
|
||||
* summary: React to a comment
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* commentDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/comment/react", async (req, res) => {
|
||||
let userid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -258,6 +686,30 @@ DB.getDB.then((DB) => {
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/comment/unreact:
|
||||
* post:
|
||||
* summary: Remove a reaction from a comment
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* postid:
|
||||
* type: string
|
||||
* commentDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/comment/unreact", async (req, res) => {
|
||||
let profileid = getProfileId(req);
|
||||
let postid = req.body.postid;
|
||||
@@ -268,6 +720,29 @@ DB.getDB.then((DB) => {
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/images:
|
||||
* get:
|
||||
* summary: Get all image posts for the current user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* posts:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/images", async (req, res) => {
|
||||
const profileid = getProfileId(req);
|
||||
const posts = await DB.getMediaTagPostOfUser(profileid, profileid);
|
||||
@@ -277,6 +752,29 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/embedded:
|
||||
* get:
|
||||
* summary: Get all embedded posts for the current user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* posts:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/embedded", async (req, res) => {
|
||||
const profileid = getProfileId(req);
|
||||
const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@iframe:");
|
||||
@@ -286,6 +784,29 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/media:
|
||||
* get:
|
||||
* summary: Get all media posts for the current user
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* posts:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/media", async (req, res) => {
|
||||
const profileid = getProfileId(req);
|
||||
const posts = await DB.getMediaTagPostOfUser(profileid, profileid, "@youtube:|@vimeo:|@hls:");
|
||||
@@ -295,6 +816,74 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/course/recent:
|
||||
* get:
|
||||
* summary: Get recently watched media from courses
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* recentMedia:
|
||||
* type: object
|
||||
* additionalProperties:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* watchRecord:
|
||||
* type: object
|
||||
* _id:
|
||||
* type: string
|
||||
* profileid:
|
||||
* type: string
|
||||
* content:
|
||||
* type: string
|
||||
* createdAt:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* reactions:
|
||||
* type: object
|
||||
* comments:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* bookmarks:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* nonOrganicType:
|
||||
* type: string
|
||||
* contentHistory:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* toProfile:
|
||||
* type: string
|
||||
* lastUpdated:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* tags:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* chatSenderId:
|
||||
* type: string
|
||||
* mediaProfileMap:
|
||||
* type: object
|
||||
* additionalProperties:
|
||||
* type: object
|
||||
*/
|
||||
router.get("/course/recent", async (req, res) => {
|
||||
const profileid = getProfileId(req);
|
||||
const profile = await DB.getProfileCache(profileid);
|
||||
@@ -342,6 +931,28 @@ DB.getDB.then((DB) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /post/{id}:
|
||||
* get:
|
||||
* summary: Get a specific post by ID
|
||||
* tags: [Posts]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Post'
|
||||
*/
|
||||
router.get("/:id", async (req, res) => {
|
||||
const postId = req.params.id;
|
||||
const post = await DB.getPost(postId);
|
||||
|
||||
@@ -4,26 +4,112 @@ var router = express.Router()
|
||||
const DB = require("../mongoDB.js");
|
||||
const Profile = require("../def/profile.js");
|
||||
const Notifications = require("./../notifications.js");
|
||||
const { getSessionId, getUserId, getProfileId } = require("./../utils/sessionUtils.js");
|
||||
|
||||
DB.getDB.then((DB) => {
|
||||
|
||||
const getUserId = function(req){
|
||||
const user_sid = req.cookies.user_sid || req.query.user_sid || req.body.user_sid;
|
||||
return DB.ObjectID(user_sid);
|
||||
}
|
||||
|
||||
const getProfileId = (req)=>{
|
||||
return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id);
|
||||
}
|
||||
|
||||
const profileBelongsToUser = async (profileid, userid) => {
|
||||
const profile = await DB.getProfileCache(profileid);
|
||||
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) => {
|
||||
let userid = req.cookies.user_sid;
|
||||
let userid = getUserId(req);
|
||||
let profiles = await DB.getUserProfiles(userid);
|
||||
return res.json({
|
||||
status: "ok",
|
||||
@@ -31,6 +117,29 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/new:
|
||||
* get:
|
||||
* summary: (DEPRECATED) Create a new profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: content
|
||||
* required: true
|
||||
* schema:
|
||||
* type: object
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/new", async (req, res) => { //Deprecated please use route post("/")
|
||||
let profile = {
|
||||
userid: getUserId(req),
|
||||
@@ -44,39 +153,141 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user:
|
||||
* post:
|
||||
* summary: Create a new profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.post("/", async (req, res) => {
|
||||
let profile = {
|
||||
userid: getUserId(req),
|
||||
...req.body.content
|
||||
};
|
||||
try {
|
||||
let profileObj = new Profile(profile);
|
||||
let r = await DB.newProfile(profileObj);
|
||||
return res.json({
|
||||
status: "ok",
|
||||
...profileObj.toObj()
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/invite", async (req, res) => {
|
||||
const userid = getUserId(req);
|
||||
const name = req.body.name;
|
||||
const email = req.body.email;
|
||||
//validate email?
|
||||
if(!name || !email) return res.json({status: "incomplete request"});
|
||||
let r = await DB.newInvitation(userid, name, email);
|
||||
if(!r.toLowerCase){
|
||||
//send email invitation
|
||||
let senderProfile = await DB.getProfile(getProfileId(req));
|
||||
Notifications.youHaveAnInvitation(name, email, senderProfile);
|
||||
} catch (error) {
|
||||
console.error("Error creating profile", error);
|
||||
return res.json({
|
||||
status: "ok"
|
||||
status: error,
|
||||
});
|
||||
}
|
||||
return res.json({
|
||||
status: r
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/invite:
|
||||
* post:
|
||||
* summary: Invite a new user by email
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 400:
|
||||
* description: Bad request
|
||||
*/
|
||||
router.post("/invite", async (req, res) => {
|
||||
try {
|
||||
const userid = getUserId(req);
|
||||
let { name, email } = req.body; // Destructuring for clarity
|
||||
// Validate required fields
|
||||
if (!name || !email) {
|
||||
return res.status(400).json({ status: "Name and email are required" });
|
||||
}
|
||||
// Validate email format
|
||||
email = email.trim().toLowerCase()
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return res.status(400).json({ status: "Invalid email format" });
|
||||
}
|
||||
// Create new invitation, this returns a string if failed
|
||||
let r = await DB.newInvitation(userid, name, email);
|
||||
if (r instanceof String) {
|
||||
// Handle failure response from DB.newInvitation
|
||||
return res.status(400).json({
|
||||
status: r,
|
||||
message: `Failed to send invitation: ${r}`
|
||||
});
|
||||
}
|
||||
// Handle response from DB.newInvitation
|
||||
// Send email invitation
|
||||
let senderProfile = await DB.getProfile(getProfileId(req));
|
||||
Notifications.youHaveAnInvitation(name, email, senderProfile);
|
||||
return res.status(200).json({
|
||||
status: "ok",
|
||||
message: `Invitation sent to ${name} (${email})`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during invitation process:", error);
|
||||
return res.status(500).json({ status: "error", message: "Something went wrong, please try again later" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/invite/{email}:
|
||||
* get:
|
||||
* summary: Get invitation details for an email
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: email
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* format: email
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* invitation:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*
|
||||
*/
|
||||
router.get("/invite/:email", async (req, res) => {
|
||||
const userid = getUserId(req);
|
||||
const email = req.params.email;
|
||||
@@ -89,6 +300,29 @@ DB.getDB.then((DB)=>{
|
||||
return res.json({ status: "ok", ...r });
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups:
|
||||
* get:
|
||||
* summary: Get a list of all groups
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* groups:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/groups", async (req, res) => {
|
||||
let groups = await DB.getGroups();
|
||||
return res.json({
|
||||
@@ -97,6 +331,29 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/following:
|
||||
* get:
|
||||
* summary: Get a list of groups the current profile is following
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* groups:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/groups/following", async (req, res) => {
|
||||
const profileId = getProfileId(req);
|
||||
let groups = await DB.getFollowingGroups(profileId);
|
||||
@@ -106,6 +363,28 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups:
|
||||
* post:
|
||||
* summary: Create a new group
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.post("/groups", async (req, res) => {
|
||||
let profile = {
|
||||
userid: getUserId(req),
|
||||
@@ -120,6 +399,29 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/courses:
|
||||
* get:
|
||||
* summary: Get a list of all courses
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* groups:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/courses", async (req, res) => {
|
||||
let groups = await DB.getCourses();
|
||||
return res.json({
|
||||
@@ -128,6 +430,29 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/accept:
|
||||
* post:
|
||||
* summary: Accept a request to join a private group
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* groupid:
|
||||
* type: string
|
||||
* profileid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/groups/accept", async (req, res) => {
|
||||
//This function should be called to accept the join request
|
||||
//of an user that attempt to join a private group.
|
||||
@@ -147,6 +472,29 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/reject:
|
||||
* post:
|
||||
* summary: Reject a request to join a private group
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* groupid:
|
||||
* type: string
|
||||
* profileid:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/groups/reject", async (req, res) => {
|
||||
//This function should be called to reject the join request
|
||||
//of an user that attempt to join a private group.
|
||||
@@ -166,6 +514,39 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/search:
|
||||
* get:
|
||||
* summary: Search for groups
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: courses
|
||||
* schema:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* groups:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/groups/search", async (req, res) => {
|
||||
let query = req.query.query;
|
||||
let coursesB = req.query.courses ? true : false;
|
||||
@@ -176,6 +557,33 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/{id}:
|
||||
* get:
|
||||
* summary: Get details for a specific group
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* groups:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/groups/:id", async (req, res) => {
|
||||
const groupid = req.params.id;
|
||||
let groups = await DB.getGroup(groupid);
|
||||
@@ -185,6 +593,24 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/{id}/subscribe:
|
||||
* get:
|
||||
* summary: Subscribe to a group
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/groups/:id/subscribe", async (req, res) => {
|
||||
const groupid = req.params.id;
|
||||
const profileid = getProfileId(req);
|
||||
@@ -197,6 +623,24 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/groups/{id}/unsubscribe:
|
||||
* get:
|
||||
* summary: Unsubscribe from a group
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/groups/:id/unsubscribe", async (req, res) => {
|
||||
const groupid = req.params.id;
|
||||
const profileid = getProfileId(req);
|
||||
@@ -207,6 +651,35 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/search:
|
||||
* get:
|
||||
* summary: Search for profiles
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* profiles:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/search", async (req, res) => {
|
||||
let query = req.query.query;
|
||||
let profiles = await DB.searchProfile(query);
|
||||
@@ -216,6 +689,36 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/setData:
|
||||
* post:
|
||||
* summary: Set custom data for a profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* key:
|
||||
* type: string
|
||||
* value:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
router.post("/setData", (req, res) => {
|
||||
const key = req.body.key;
|
||||
const value = req.body.value;
|
||||
@@ -226,6 +729,36 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/myProfile:
|
||||
* post:
|
||||
* summary: Update the current user's profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* profile:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
* data:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
router.post("/myProfile", async (req, res) => {
|
||||
let profile = {
|
||||
userid: getUserId(req),
|
||||
@@ -239,15 +772,74 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/{id}:
|
||||
* get:
|
||||
* summary: Get a specific profile by ID
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Profile'
|
||||
*/
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
let profileId = req.params.id;
|
||||
let profile = await DB.getProfile(profileId);
|
||||
if (!profile || !profile._id) {
|
||||
return res.status(404).json({
|
||||
status: "Profile not found",
|
||||
});
|
||||
}
|
||||
return res.json({
|
||||
status: "ok",
|
||||
...profile
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading profile", error);
|
||||
return res.status(500).json({
|
||||
status: "Internal server error"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/{id}:
|
||||
* delete:
|
||||
* summary: Delete a profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const profileId = req.params.id;
|
||||
const userid = getUserId(req);
|
||||
@@ -261,6 +853,31 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/{id}/follow:
|
||||
* get:
|
||||
* summary: Follow a profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
router.get("/:id/follow", async (req, res) => {
|
||||
let followProfileId = req.params.id;
|
||||
const profileid = getProfileId(req);
|
||||
@@ -271,6 +888,31 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /user/{id}/unfollow:
|
||||
* get:
|
||||
* summary: Unfollow a profile
|
||||
* tags: [Profiles]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
router.get("/:id/unfollow", async (req, res) => {
|
||||
let followProfileId = req.params.id;
|
||||
const profileid = getProfileId(req);
|
||||
|
||||
100
routes/songs.js
100
routes/songs.js
@@ -15,6 +15,25 @@ DB.getDB.then((DB)=>{
|
||||
return DB.ObjectID(req.cookies.profile_id || req.query.profile_id || req.body.profile_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Songs
|
||||
* description: Song management
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /songs:
|
||||
* get:
|
||||
* summary: Get all songs
|
||||
* tags: [Songs]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
let profileId = req.params.id;
|
||||
let songs = await DB.getSongs();
|
||||
@@ -24,6 +43,24 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /songs:
|
||||
* post:
|
||||
* summary: Create a new song
|
||||
* tags: [Songs]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/", async (req, res) => {
|
||||
let post = {
|
||||
userid: getUserId(req),
|
||||
@@ -39,6 +76,24 @@ DB.getDB.then((DB)=>{
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /songs/{id}:
|
||||
* get:
|
||||
* summary: Get a specific song by ID
|
||||
* tags: [Songs]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/:id", async (req, res) => {
|
||||
let profileId = req.params.id;
|
||||
let profile = await DB.getProfile(profileId);
|
||||
@@ -53,6 +108,24 @@ DB.getDB.then((DB)=>{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /songs/{id}:
|
||||
* delete:
|
||||
* summary: Delete a song
|
||||
* tags: [Songs]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const userid = getUserId(req);
|
||||
const songId = req.params.id;
|
||||
@@ -66,6 +139,33 @@ DB.getDB.then((DB)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /songs/{id}:
|
||||
* post:
|
||||
* summary: Update a song
|
||||
* tags: [Songs]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* content:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.post("/:id", async (req, res) => {
|
||||
const userid = getUserId(req);
|
||||
const songId = req.params.id;
|
||||
|
||||
@@ -67,12 +67,51 @@ const getMedia = async (eventId) => {
|
||||
};
|
||||
|
||||
//getMedia('y42zyf3').then(console.log)
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Subsplash
|
||||
* description: Subsplash API integration
|
||||
*/
|
||||
|
||||
DB.getDB.then((DB) => {
|
||||
/**
|
||||
* @swagger
|
||||
* /subsplash/events/{calendarId}:
|
||||
* get:
|
||||
* summary: Get events from a Subsplash calendar
|
||||
* tags: [Subsplash]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: calendarId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/events/:calendarId", async (req, res) => {
|
||||
const events = await getEvents(req.params.calendarId)
|
||||
return res.json(events);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /subsplash/media/{seriesId}:
|
||||
* get:
|
||||
* summary: Get media from a Subsplash media series
|
||||
* tags: [Subsplash]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: seriesId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
*/
|
||||
router.get("/media/:seriesId", async (req, res) => {
|
||||
const events = await getMedia(req.params.seriesId)
|
||||
return res.json(events);
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
const { ObjectId } = require("mongodb");
|
||||
|
||||
const isValidObjectId = (id) => ObjectId.isValid(id);
|
||||
|
||||
// Utilities
|
||||
const getSessionId = function (req) {
|
||||
const session_id = req.cookies.session_id || req.query.session_id || req.body.session_id;
|
||||
if(!session_id) {
|
||||
return session_id;
|
||||
}
|
||||
if(isValidObjectId(session_id)) {
|
||||
return session_id;
|
||||
}
|
||||
console.trace();
|
||||
console.error("Invalid session_id format: ", session_id);
|
||||
return session_id;
|
||||
}
|
||||
const getUserId = function (req) {
|
||||
const user_sid = req.cookies.user_sid || req.query.user_sid || req.body.user_sid;
|
||||
// validate user_sid
|
||||
if(!user_sid) {
|
||||
return user_sid;
|
||||
}
|
||||
if(isValidObjectId(user_sid)) {
|
||||
return user_sid;
|
||||
}
|
||||
console.trace();
|
||||
console.error("Invalid user_sid format: ", user_sid);
|
||||
return user_sid;
|
||||
}
|
||||
const getProfileId = function (req) {
|
||||
const profile_id = req.cookies.profile_id || req.query.profile_id || req.body.profile_id;
|
||||
if(!profile_id) {
|
||||
return profile_id;
|
||||
}
|
||||
if(isValidObjectId(profile_id)) {
|
||||
return profile_id;
|
||||
}
|
||||
console.trace();
|
||||
console.error("Invalid profile_id format: ", profile_id);
|
||||
return profile_id;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user