From 1df7541a5124093b202fd11de9ebe89febc305b3 Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 3 Jun 2026 10:20:07 -0700 Subject: [PATCH 1/6] fix: update environment variable setup and add example file --- README.md | 7 ++++--- packages/backend/.env.example | 3 +++ packages/backend/config/env.js | 29 +++++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 packages/backend/.env.example diff --git a/README.md b/README.md index a465afd..4cfa6e9 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ For anyone who needs motivation to focus students, professionals, business owner - ensure no linting errors before pushing code - ### Development Enviroment - At root run "npm i" to install all packages - - in `packages/backend/.env` + - Copy `packages/backend/.env.example` to `packages/backend/.env` - Set `MONGO_URI` to the MongoDB Atlas connection string - - For JWT auth, set `JWT_SECRET` + - Replace `` and `` with a MongoDB Atlas database user + - For JWT auth, set `JWT_SECRET` to a long random value - In one terminal start the backend: npm -w backend run dev - In another terminal: npm -w frontend run dev - The backend defaults to port 5050, and the frontend dev server proxies `/api` requests there. @@ -34,4 +35,4 @@ For anyone who needs motivation to focus students, professionals, business owner - https://lucid.app/lucidchart/b21e47c0-d97b-4f2c-a8d7-765136d80f12/edit?viewport_loc=462%2C-16%2C2012%2C971%2C0_0&invitationId=inv_159c0efd-5942-40aa-9582-62b534aa5c56 - last updated: 5/29/26 -Demo Video: \ No newline at end of file +Demo Video: diff --git a/packages/backend/.env.example b/packages/backend/.env.example new file mode 100644 index 0000000..b5d1127 --- /dev/null +++ b/packages/backend/.env.example @@ -0,0 +1,3 @@ +MONGO_URI=mongodb+srv://:@thegoats-project-cluste.x5ukfgi.mongodb.net/?retryWrites=true&w=majority&appName=thegoats-project-cluster +JWT_SECRET=replace-with-a-long-random-secret +PORT=5050 diff --git a/packages/backend/config/env.js b/packages/backend/config/env.js index 6fb9c4c..452afc6 100644 --- a/packages/backend/config/env.js +++ b/packages/backend/config/env.js @@ -7,8 +7,10 @@ const backendRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "..", ); +const workspaceRoot = path.resolve(backendRoot, "..", ".."); dotenv.config({ path: path.join(backendRoot, ".env"), quiet: true }); +dotenv.config({ path: path.join(workspaceRoot, ".env"), quiet: true }); export const MONGO_URI = process.env.MONGO_URI; export const JWT_SECRET = process.env.JWT_SECRET; @@ -22,8 +24,31 @@ const missingEnvVars = [ .filter(([, value]) => !value) .map(([name]) => name); -if (missingEnvVars.length > 0) { +const placeholderEnvVars = [ + ["MONGO_URI", MONGO_URI, ["", ""]], + ["JWT_SECRET", JWT_SECRET, ["replace-with", "change-me"]], +] + .filter(([, value, placeholders]) => + placeholders.some((placeholder) => value?.includes(placeholder)), + ) + .map(([name]) => name); + +if (missingEnvVars.length > 0 || placeholderEnvVars.length > 0) { + const envFile = path.join(backendRoot, ".env"); + const exampleFile = path.join(backendRoot, ".env.example"); + const envIssues = [ + missingEnvVars.length > 0 ? `Missing: ${missingEnvVars.join(", ")}` : null, + placeholderEnvVars.length > 0 + ? `Still using placeholder values: ${placeholderEnvVars.join(", ")}` + : null, + ].filter(Boolean); + throw new Error( - `Missing required environment variables: ${missingEnvVars.join(", ")}`, + [ + `Invalid backend environment configuration. ${envIssues.join("; ")}.`, + `Create ${envFile} from ${exampleFile} and fill in your Atlas database user credentials.`, + "Use a MongoDB Atlas database user/password, not your MongoDB Cloud account password.", + "If the password contains special characters, URL-encode it before putting it in MONGO_URI.", + ].join("\n"), ); } From d3bb6f988d1c948d9d38c287d59e6d0c1ce6d7a0 Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 3 Jun 2026 12:34:53 -0700 Subject: [PATCH 2/6] feat(users): add account deletion endpoint and cleanup logic - Add deleteUserAccount helper function to handle cascading deletions * Delete all groups owned by the user * Remove user from all groups they're a member of * Delete the user account itself - Add DELETE /:userParam endpoint for account deletion * Verify user can only delete their own account * Extract user by ID or email using existing getUserFilter logic * Clear authentication cookie on successful deletion to log out user * Return appropriate error responses (404 for not found, 403 for unauthorized) - Import AUTH_COOKIE_NAME and clearAuthCookieOptions from auth config - Ensures data integrity when users delete their accounts by cleaning up group associations --- packages/backend/routes/users.js | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/backend/routes/users.js b/packages/backend/routes/users.js index b78e214..d776edb 100644 --- a/packages/backend/routes/users.js +++ b/packages/backend/routes/users.js @@ -5,6 +5,7 @@ import mongoose from "mongoose"; import User from "../models/user.js"; import Group from "../models/group.js"; import requireAuth from "../middleware/requireAuth.js"; +import { AUTH_COOKIE_NAME, clearAuthCookieOptions } from "../config/auth.js"; const router = express.Router(); @@ -88,6 +89,24 @@ function getUserFilter(userParam) { return { email: value }; } +export async function deleteUserAccount(userId) { + const ownedGroups = await Group.find({ owner: userId }).select("_id"); + const ownedGroupIds = ownedGroups.map((group) => group._id); + + if (ownedGroupIds.length > 0) { + await Group.deleteMany({ _id: { $in: ownedGroupIds } }); + + await User.updateMany( + { groups: { $in: ownedGroupIds } }, + { $pull: { groups: { $in: ownedGroupIds } } }, + ); + } + + await Group.updateMany({ users: userId }, { $pull: { users: userId } }); + + return User.findByIdAndDelete(userId); +} + //update current user's commitment router.patch("/me/commitment", requireAuth, async (req, res) => { try { @@ -287,4 +306,40 @@ router.put("/:userParam", requireAuth, async (req, res) => { } }); +// delete a user account by id/email — called from the frontend as: +// fetch(`${AZURE_URL}/api/users/${user._id}`, { method: "DELETE", credentials: "include" }) +// requireAuth handles token extraction from cookie or Authorization header, +// then we verify the caller is only deleting their own account. +router.delete("/:userParam", requireAuth, async (req, res) => { + try { + const filter = getUserFilter(req.params.userParam); + const targetUser = await User.findOne(filter).select("_id"); + + if (!targetUser) { + return res.status(404).json({ error: "User not found" }); + } + + // a user may only delete their own account + if (targetUser._id.toString() !== req.auth.userId) { + return res.status(403).json({ error: "You can only delete yourself" }); + } + + const deletedUser = await deleteUserAccount(targetUser._id); + + if (!deletedUser) { + return res.status(404).json({ error: "User not found" }); + } + + // clear the session cookie so the client is immediately logged out + res.clearCookie(AUTH_COOKIE_NAME, clearAuthCookieOptions); + + return res.json({ + message: "Account deleted successfully", + deletedUserId: targetUser._id, + }); + } catch (error) { + return sendError(res, error, 400); + } +}); + export default router; From b98dfef8f158e211d701c1384a04268bad9a83e7 Mon Sep 17 00:00:00 2001 From: Alfredo <193377562+A-Galicia@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:02:01 -0700 Subject: [PATCH 3/6] revert changes --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4cfa6e9..4a8ec3a 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,9 @@ For anyone who needs motivation to focus students, professionals, business owner - ensure no linting errors before pushing code - ### Development Enviroment - At root run "npm i" to install all packages - - Copy `packages/backend/.env.example` to `packages/backend/.env` + - in `packages/backend/.env` - Set `MONGO_URI` to the MongoDB Atlas connection string - - Replace `` and `` with a MongoDB Atlas database user - - For JWT auth, set `JWT_SECRET` to a long random value + - For JWT auth, set `JWT_SECRET` - In one terminal start the backend: npm -w backend run dev - In another terminal: npm -w frontend run dev - The backend defaults to port 5050, and the frontend dev server proxies `/api` requests there. From 443920db692c6718ea49739b78638ce6e620b49c Mon Sep 17 00:00:00 2001 From: Alfredo <193377562+A-Galicia@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:02:37 -0700 Subject: [PATCH 4/6] revert changes --- packages/backend/config/env.js | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/backend/config/env.js b/packages/backend/config/env.js index 452afc6..6fb9c4c 100644 --- a/packages/backend/config/env.js +++ b/packages/backend/config/env.js @@ -7,10 +7,8 @@ const backendRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "..", ); -const workspaceRoot = path.resolve(backendRoot, "..", ".."); dotenv.config({ path: path.join(backendRoot, ".env"), quiet: true }); -dotenv.config({ path: path.join(workspaceRoot, ".env"), quiet: true }); export const MONGO_URI = process.env.MONGO_URI; export const JWT_SECRET = process.env.JWT_SECRET; @@ -24,31 +22,8 @@ const missingEnvVars = [ .filter(([, value]) => !value) .map(([name]) => name); -const placeholderEnvVars = [ - ["MONGO_URI", MONGO_URI, ["", ""]], - ["JWT_SECRET", JWT_SECRET, ["replace-with", "change-me"]], -] - .filter(([, value, placeholders]) => - placeholders.some((placeholder) => value?.includes(placeholder)), - ) - .map(([name]) => name); - -if (missingEnvVars.length > 0 || placeholderEnvVars.length > 0) { - const envFile = path.join(backendRoot, ".env"); - const exampleFile = path.join(backendRoot, ".env.example"); - const envIssues = [ - missingEnvVars.length > 0 ? `Missing: ${missingEnvVars.join(", ")}` : null, - placeholderEnvVars.length > 0 - ? `Still using placeholder values: ${placeholderEnvVars.join(", ")}` - : null, - ].filter(Boolean); - +if (missingEnvVars.length > 0) { throw new Error( - [ - `Invalid backend environment configuration. ${envIssues.join("; ")}.`, - `Create ${envFile} from ${exampleFile} and fill in your Atlas database user credentials.`, - "Use a MongoDB Atlas database user/password, not your MongoDB Cloud account password.", - "If the password contains special characters, URL-encode it before putting it in MONGO_URI.", - ].join("\n"), + `Missing required environment variables: ${missingEnvVars.join(", ")}`, ); } From 0a1879c52e7d429090599bd505114c645531163d Mon Sep 17 00:00:00 2001 From: Alfredo <193377562+A-Galicia@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:05:50 -0700 Subject: [PATCH 5/6] deleted comments --- packages/backend/routes/users.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/backend/routes/users.js b/packages/backend/routes/users.js index d776edb..9e9d421 100644 --- a/packages/backend/routes/users.js +++ b/packages/backend/routes/users.js @@ -306,10 +306,6 @@ router.put("/:userParam", requireAuth, async (req, res) => { } }); -// delete a user account by id/email — called from the frontend as: -// fetch(`${AZURE_URL}/api/users/${user._id}`, { method: "DELETE", credentials: "include" }) -// requireAuth handles token extraction from cookie or Authorization header, -// then we verify the caller is only deleting their own account. router.delete("/:userParam", requireAuth, async (req, res) => { try { const filter = getUserFilter(req.params.userParam); From cb3378bf94f37131d64557fb34cda5124c0ca65a Mon Sep 17 00:00:00 2001 From: Alfredo <193377562+A-Galicia@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:10:32 -0700 Subject: [PATCH 6/6] fix: deleted example .env --- packages/backend/.env.example | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 packages/backend/.env.example diff --git a/packages/backend/.env.example b/packages/backend/.env.example deleted file mode 100644 index b5d1127..0000000 --- a/packages/backend/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -MONGO_URI=mongodb+srv://:@thegoats-project-cluste.x5ukfgi.mongodb.net/?retryWrites=true&w=majority&appName=thegoats-project-cluster -JWT_SECRET=replace-with-a-long-random-secret -PORT=5050