Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d907aeecee | |||
| 1ca38ca3b9 | |||
| e13678ad56 | |||
| bbc8c36439 | |||
| 822e2bc0d6 |
@@ -0,0 +1,100 @@
|
||||
# EMI Backend Agent Notes
|
||||
|
||||
## What this service is
|
||||
- Node.js + Express API for EMI social features (profiles, posts, groups/courses, songs, payments, Bible/subsplash integrations).
|
||||
- Main entrypoint: `index.js`.
|
||||
- MongoDB Atlas-backed via `MONGO_URL` using `mongodb@3.6.x`.
|
||||
|
||||
## Runbook
|
||||
- Install: `npm install`
|
||||
- Start: `npm start` (binds to `PORT`, default `3000`)
|
||||
- Test: `npm test` (single auth test file)
|
||||
- API docs: `GET /api-docs`
|
||||
|
||||
## High-level architecture
|
||||
- `index.js`: middleware setup, auth routes, route mounting, Swagger, web-push setup.
|
||||
- `mongoDB.js`: creates shared DB object + collections + utility methods, then extends with:
|
||||
- `dbTools/profile.js`
|
||||
- `dbTools/post.js`
|
||||
- `dbTools/payments.js`
|
||||
- `dbTools/songs.js`
|
||||
- `middleware/sessionChecker.js`: cookie/session validation and profile context hydration.
|
||||
- `routes/*.js`: feature-specific routers.
|
||||
- `def/*.js`: lightweight constructors for `Profile`, `Post`, `Songs`.
|
||||
|
||||
## Auth + session model
|
||||
- Cookies used:
|
||||
- `user_sid`
|
||||
- `session_id`
|
||||
- `profile_id`
|
||||
- `sessionChecker` verifies ObjectId format, then checks session in `tokens` collection.
|
||||
- On missing/invalid session/profile, user is redirected to `/login`.
|
||||
- Most app routes are protected with `sessionChecker` except:
|
||||
- `/signup`, `/login`, `/logout`, `/resetPassword`
|
||||
- `/payments/*`
|
||||
- `/subsplash/*`
|
||||
- `/invite/:email`
|
||||
|
||||
## Key route surfaces
|
||||
- `routes/profile.js`:
|
||||
- Profile CRUD, invites, follow/unfollow, group/course discovery, subscribe/approve/reject flows.
|
||||
- `routes/post.js`:
|
||||
- Feed endpoints, tags/media filters, create/edit/delete posts, reactions/comments/bookmarks.
|
||||
- Merges organic + non-organic posts (news/popular recommendations).
|
||||
- `routes/payments.js`:
|
||||
- Stripe payment intent creation + result registration; can toggle subscription timestamp.
|
||||
- `routes/songs.js`:
|
||||
- Song CRUD (ownership checks are effectively placeholder).
|
||||
- `routes/bible.js`:
|
||||
- Proxies scripture.api.bible endpoints using hardcoded API key in source.
|
||||
- `routes/subsplash.js`:
|
||||
- Scrapes Subsplash HTML with cheerio for events/media.
|
||||
|
||||
## Data model (collections)
|
||||
- `users`: auth identity + password hash + optional customer.
|
||||
- `tokens`: session documents (`uid` points to user).
|
||||
- `invitation`: invite gating for signup.
|
||||
- `profiles`: user/group/course/chat profile documents.
|
||||
- `posts`: feed posts, reactions, comments, bookmarks, tags, non-organic type.
|
||||
- `payments`: intent and payment result records.
|
||||
- `songs`: song content metadata and reactions/comments.
|
||||
|
||||
## Important operational dependencies
|
||||
- Mongo connection is required before server starts listening (`index.js` waits for `DB.getDB`).
|
||||
- Notifications:
|
||||
- Email via `nodemailer` SMTP (`mail.emmint.com`, env `EMAILPASS`).
|
||||
- Mobile push via Expo (`expo-server-sdk`).
|
||||
- Web push VAPID keys (`PUBLIC_VAPID_KEY`, `PRIVATE_VAPID_KEY`, `WEB_PUSH_EMAIL`).
|
||||
- Analytics via PostHog (`POSTHOG_API_KEY`).
|
||||
- Stripe via `STRIPE`.
|
||||
|
||||
## Environment/cookie/cors behavior
|
||||
- Cookies configured in `config/cookiesOptions.js`:
|
||||
- production or `COOKIE_SECURE=true` => `secure: true`, `sameSite: none`
|
||||
- local HTTP => `secure: false`, `sameSite: lax`
|
||||
- Allowed CORS origins in `config/corsOptions.js` are explicit list-based.
|
||||
|
||||
## Known code risks and maintenance hotspots
|
||||
- Mixed ESM/CommonJS utility scripts (`AITools.js` uses ESM style while app is CommonJS).
|
||||
- `routes/bible.js` has duplicate `/books` route and a probable bug in `/books/:bookId` (`bibleId` reference).
|
||||
- Hardcoded external API key in `routes/bible.js` should be moved to env.
|
||||
- `routes/songs.js` `songBelongsToUser` always returns true (authorization gap).
|
||||
- Some endpoints return redirect-to-login for API callers instead of structured 401 JSON.
|
||||
- Inconsistent error handling/response shapes across routes.
|
||||
- Legacy driver/runtime tension:
|
||||
- Dependency is `mongodb@3.6.x`
|
||||
- `Dockerfile` uses Node 22, but code warns Node 22 is not fully tested; Node 20 LTS is safer.
|
||||
|
||||
## Testing state
|
||||
- Only `test/auth.test.js` exists; no broad coverage for routes/db tools.
|
||||
- Auth test expects existing seeded user behavior, so reliability depends on DB fixture state.
|
||||
|
||||
## Suggested workflow for future changes
|
||||
- Keep fixes scoped and defensive (null checks + stable JSON).
|
||||
- For auth/session changes:
|
||||
- update both `sessionChecker` and `utils/sessionUtils.js`.
|
||||
- For profile/post behavior:
|
||||
- confirm DB helper method side effects in `dbTools/*`.
|
||||
- For production incidents:
|
||||
- first validate `MONGO_URL` connectivity and cookie security mode alignment.
|
||||
|
||||
+28
-2
@@ -23,7 +23,9 @@ userDB = (DB) => {
|
||||
|
||||
DB.updateProfile = async (profileid, profileObj) => {
|
||||
let tempProfile = profileObj.toObj();
|
||||
const query = { _id: profileid };
|
||||
if (!DB.ObjectID.isValid(profileid)) return false;
|
||||
const _id = DB.ObjectID(profileid);
|
||||
const query = { _id };
|
||||
const update = {
|
||||
$set: {
|
||||
profile: tempProfile.profile,
|
||||
@@ -34,6 +36,7 @@ userDB = (DB) => {
|
||||
console.log(err);
|
||||
return false;
|
||||
});
|
||||
if (userProfileCache[profileid]) delete userProfileCache[profileid];
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -281,13 +284,36 @@ userDB = (DB) => {
|
||||
postid,
|
||||
commentIndx,
|
||||
actorid,
|
||||
viewed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
return DB.profileCols.updateOne({ _id }, update).catch((err) => {
|
||||
const r = await DB.profileCols.updateOne({ _id }, update).catch((err) => {
|
||||
console.log(err);
|
||||
return false;
|
||||
});
|
||||
if (userProfileCache[profileid]) delete userProfileCache[profileid];
|
||||
return r;
|
||||
}
|
||||
|
||||
DB.markNotificationsViewed = async (profileid) => {
|
||||
const _id = DB.ObjectID(profileid);
|
||||
const update = {
|
||||
$set: {
|
||||
"notifications.$[n].viewed": true
|
||||
}
|
||||
};
|
||||
const options = {
|
||||
arrayFilters: [
|
||||
{ "n.viewed": { $ne: true } }
|
||||
]
|
||||
};
|
||||
const r = await DB.profileCols.updateOne({ _id }, update, options).catch((err) => {
|
||||
console.log(err);
|
||||
return false;
|
||||
});
|
||||
if (userProfileCache[profileid]) delete userProfileCache[profileid];
|
||||
return r;
|
||||
}
|
||||
|
||||
DB.isSubscriptor = async (profileid) => {
|
||||
|
||||
+42
-10
@@ -760,16 +760,48 @@ DB.getDB.then((DB) => {
|
||||
* type: string
|
||||
*/
|
||||
router.post("/myProfile", async (req, res) => {
|
||||
let profile = {
|
||||
userid: getUserId(req),
|
||||
profile: req.body.profile,
|
||||
data: req.body.data
|
||||
};
|
||||
let profileObj = new Profile(profile); //validates profile
|
||||
DB.updateProfile(getProfileId(req), profileObj);
|
||||
return res.json({
|
||||
status: "ok"
|
||||
});
|
||||
try {
|
||||
let profile = {
|
||||
userid: getUserId(req),
|
||||
profile: req.body.profile,
|
||||
data: req.body.data
|
||||
};
|
||||
let profileObj = new Profile(profile); //validates profile
|
||||
const updateRes = await DB.updateProfile(getProfileId(req), profileObj);
|
||||
if (!updateRes || !updateRes.matchedCount) {
|
||||
return res.status(400).json({
|
||||
status: "Could not update profile"
|
||||
});
|
||||
}
|
||||
return res.json({
|
||||
status: "ok"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating myProfile", error);
|
||||
return res.status(500).json({
|
||||
status: "Internal server error"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/notifications/viewed", async (req, res) => {
|
||||
try {
|
||||
const profileid = getProfileId(req);
|
||||
const result = await DB.markNotificationsViewed(profileid);
|
||||
if (!result) {
|
||||
return res.status(400).json({
|
||||
status: "Could not update notifications"
|
||||
});
|
||||
}
|
||||
return res.json({
|
||||
status: "ok"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error marking notifications as viewed", error);
|
||||
return res.status(500).json({
|
||||
status: "Internal server error"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user