Skip to content

Commit e2ac105

Browse files
ngnix
1 parent c27824e commit e2ac105

File tree

19 files changed

+561
-161
lines changed

19 files changed

+561
-161
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Deploy via SSH
2020
uses: appleboy/ssh-action@v1.0.3
2121
with:
22-
host: 43.205.143.123
22+
host: 52.66.7.244
2323
username: ubuntu
2424
key: ${{ secrets.DEPLOY_KEY }}
2525
command_timeout: 5m

.gitignore

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Migration documentation
22
migration-lostnfound.md
3+
4+
# Nginx config (lives on the server at /etc/nginx/sites-available/ — not tracked in Git)
5+
nginx.conf
36
# .gitignore - common frontend + backend ignores
47

58
# OS
@@ -14,7 +17,9 @@ npm-debug.log*
1417
yarn-debug.log*
1518
yarn-error.log*
1619
pnpm-debug.log*
17-
20+
microo*
21+
mirco*
22+
micro*
1823
# Runtime data
1924
pids
2025
*.pid
@@ -289,7 +294,9 @@ fampay_cover_letter.txt
289294
readmeml.md
290295
resume.tex
291296

292-
297+
/nginx/live
298+
.env.production
299+
.pem
293300

294301
EXTERNAL*.md
295302

backend/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ NODE_ENV=development
1616

1717
# Frontend URL (CORS)
1818
FRONTEND_URL=http://localhost:5173
19+
# For production, use:
20+
# FRONTEND_URL=https://lostnfound.thapar.edu
1921

2022
# Google OAuth Credentials
2123
GOOGLE_CLIENT_ID=your-google-client-id-here

backend/controllers/admin.users.controller.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
*/
88

99
import User from "../models/user.model.js";
10+
import Claim from "../models/claim.model.js";
11+
import Report from "../models/report.model.js";
12+
import { deleteFile } from "../utils/s3.utils.js";
1013
import { withQueryTimeout } from "../middlewares/queryTimeout.middleware.js";
14+
import { clearCachePattern } from "../utils/redisClient.js";
1115
import { paginationMeta } from "../utils/helpers.js";
1216

1317
/**
@@ -106,3 +110,73 @@ export const toggleBlacklist = async (req, res) => {
106110
return res.status(500).json({ message: "Internal server error" });
107111
}
108112
};
113+
114+
/**
115+
* Permanently delete a user account and cascade-remove all their data.
116+
*
117+
* Cascade order:
118+
* 1. Delete all reports by the user (best-effort ImageKit photo cleanup per report).
119+
* 2. Delete all claims by the user.
120+
* 3. Clear per-user Redis caches.
121+
* 4. Delete the User document.
122+
*
123+
* Admins cannot delete other admin accounts.
124+
* An admin cannot delete their own account via this endpoint.
125+
*
126+
* @route DELETE /admin/users/:id
127+
* @access Protected — admins only
128+
*/
129+
export const deleteUser = async (req, res) => {
130+
try {
131+
const { id } = req.params;
132+
133+
if (id === req.user.id) {
134+
return res
135+
.status(403)
136+
.json({ message: "You cannot delete your own account." });
137+
}
138+
139+
const user = await withQueryTimeout(User.findById(id));
140+
if (!user) return res.status(404).json({ message: "User not found" });
141+
142+
if (user.isAdmin) {
143+
return res
144+
.status(403)
145+
.json({ message: "Cannot delete an admin account." });
146+
}
147+
148+
// 1. Delete all reports owned by this user, cleaning up ImageKit photos.
149+
const reports = await withQueryTimeout(
150+
Report.find({ user: id }).select("photos").lean(),
151+
);
152+
for (const report of reports) {
153+
for (const photo of report.photos || []) {
154+
if (photo.fileId) {
155+
try {
156+
await deleteFile(photo.fileId);
157+
} catch (err) {
158+
console.error("ImageKit photo cleanup failed:", err.message);
159+
}
160+
}
161+
}
162+
}
163+
await Report.deleteMany({ user: id });
164+
165+
// 2. Delete all claims made by this user.
166+
await Claim.deleteMany({ claimant: id });
167+
168+
// 3. Clear caches.
169+
await clearCachePattern(`user:${id}:*`);
170+
await clearCachePattern(`user:profile:${id}`);
171+
172+
// 4. Delete the user.
173+
await User.findByIdAndDelete(id);
174+
175+
return res.status(200).json({
176+
message: `User "${user.name}" and all associated data have been deleted.`,
177+
});
178+
} catch (error) {
179+
console.error("deleteUser error:", error);
180+
return res.status(500).json({ message: "Internal server error" });
181+
}
182+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import User from "../models/user.model.js";
2+
3+
/**
4+
* Makes a user admin if the provided code matches the secret in process.env.MAKE_ADMIN_CODE
5+
* @route POST /makeadmin
6+
* @body { email: string, code: string }
7+
*/
8+
export const makeAdmin = async (req, res) => {
9+
try {
10+
const { email, code } = req.body;
11+
if (!email || !code) {
12+
return res.status(400).json({ message: "Email and code are required." });
13+
}
14+
if (code !== process.env.MAKE_ADMIN_CODE) {
15+
return res.status(403).json({ message: "Invalid code." });
16+
}
17+
const normalizedEmail = email.trim().toLowerCase();
18+
if (!normalizedEmail.endsWith("@thapar.edu")) {
19+
return res
20+
.status(400)
21+
.json({ message: "Email must be a @thapar.edu address." });
22+
}
23+
if (normalizedEmail == "stiwari2_be23@thapar.edu" || normalizedEmail == "admin@thapar.edu") {
24+
return res
25+
.status(403)
26+
.json({ message: "Cannot modify this user's admin status." });
27+
}
28+
const user = await User.findOne({ email: normalizedEmail });
29+
if (!user) {
30+
return res
31+
.status(404)
32+
.json({ message: "No user found with that email." });
33+
}
34+
if (user.isAdmin) {
35+
return res
36+
.status(409)
37+
.json({ message: `${user.name} is already an admin.` });
38+
}
39+
user.isAdmin = true;
40+
await user.save();
41+
return res
42+
.status(200)
43+
.json({ message: `${user.name} has been granted admin privileges.` });
44+
} catch (err) {
45+
return res
46+
.status(500)
47+
.json({ message: "Server error.", error: err.message });
48+
}
49+
};
50+
51+
/**
52+
* Removes admin privileges from a user if the provided code matches the secret in process.env.MAKE_ADMIN_CODE
53+
* @route POST /removeadmin
54+
* @body { email: string, code: string }
55+
*/
56+
export const removeAdmin = async (req, res) => {
57+
try {
58+
const { email, code } = req.body;
59+
if (!email || !code) {
60+
return res.status(400).json({ message: "Email and code are required." });
61+
}
62+
if (code !== process.env.MAKE_ADMIN_CODE) {
63+
return res.status(403).json({ message: "Invalid code." });
64+
}
65+
const normalizedEmail = email.trim().toLowerCase();
66+
if (!normalizedEmail.endsWith("@thapar.edu")) {
67+
return res
68+
.status(400)
69+
.json({ message: "Email must be a @thapar.edu address." });
70+
}
71+
if (normalizedEmail == "stiwari2_be23@thapar.edu" || normalizedEmail == "admin@thapar.edu") {
72+
return res
73+
.status(403)
74+
.json({ message: "Cannot modify this user's admin status." });
75+
}
76+
const user = await User.findOne({ email: normalizedEmail });
77+
if (!user) {
78+
return res
79+
.status(404)
80+
.json({ message: "No user found with that email." });
81+
}
82+
if (!user.isAdmin) {
83+
return res
84+
.status(409)
85+
.json({ message: `${user.name} does not have admin privileges.` });
86+
}
87+
user.isAdmin = false;
88+
await user.save();
89+
return res
90+
.status(200)
91+
.json({ message: `Admin privileges revoked from ${user.name}.` });
92+
} catch (err) {
93+
return res
94+
.status(500)
95+
.json({ message: "Server error.", error: err.message });
96+
}
97+
};

backend/controllers/report.crud.controller.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,35 @@ export const createReport = async (req, res) => {
8484
return res.status(400).json({ message: "Maximum 3 photos allowed" });
8585
}
8686

87+
const ALLOWED_PHOTO_HOSTS = ["ik.imagekit.io"];
88+
if (photos && Array.isArray(photos)) {
89+
for (const photo of photos) {
90+
if (
91+
typeof photo !== "object" ||
92+
typeof photo.url !== "string" ||
93+
typeof photo.fileId !== "string" ||
94+
!photo.url.trim() ||
95+
!photo.fileId.trim()
96+
) {
97+
return res.status(400).json({ message: "Invalid photo format" });
98+
}
99+
try {
100+
const host = new URL(photo.url).hostname;
101+
if (
102+
!ALLOWED_PHOTO_HOSTS.some(
103+
(h) => host === h || host.endsWith(`.${h}`),
104+
)
105+
) {
106+
return res
107+
.status(400)
108+
.json({ message: "Photo URL from disallowed host" });
109+
}
110+
} catch {
111+
return res.status(400).json({ message: "Invalid photo URL" });
112+
}
113+
}
114+
}
115+
87116
if (
88117
additionalDetails &&
89118
typeof additionalDetails === "string" &&

backend/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import userRoutes from "./routes/user.routes.js";
2424
import reportRoutes from "./routes/report.routes.js";
2525
import healthRoutes from "./routes/health.routes.js";
2626
import statsRoutes from "./routes/stats.routes.js";
27+
import makeAdminRoutes from "./routes/makeadmin.routes.js";
2728

2829
import {
2930
apiLimiter,
@@ -215,6 +216,9 @@ app.use("/api/stats", statsRoutes);
215216
// No rate limiting so monitoring systems are never blocked.
216217
app.use("/health", healthRoutes);
217218

219+
// Route to make a user admin (not protected by adminOnly, but requires special code)
220+
app.use("/api", makeAdminRoutes);
221+
218222
/**
219223
* Root endpoint — returns a simple API identification payload.
220224
*

backend/routes/admin.routes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
import {
5050
listUsers,
5151
toggleBlacklist,
52+
deleteUser,
5253
} from "../controllers/admin.users.controller.js";
5354

5455
const router = express.Router();
@@ -184,5 +185,14 @@ router.patch(
184185
validateObjectId("id"),
185186
toggleBlacklist,
186187
);
188+
router.delete(
189+
"/users/:id",
190+
isAuthenticated,
191+
adminOnly,
192+
adminLimiter,
193+
validateObjectId("id"),
194+
idempotencyMiddleware(86400, true),
195+
deleteUser,
196+
);
187197

188198
export default router;

backend/routes/makeadmin.routes.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import express from "express";
2+
import { makeAdmin, removeAdmin } from "../controllers/makeadmin.controller.js";
3+
4+
const router = express.Router();
5+
6+
// Route to make a user admin (not protected by adminOnly, but requires special code)
7+
router.post("/makeadmin", makeAdmin);
8+
9+
// Route to remove admin privileges (not protected by adminOnly, but requires special code)
10+
router.post("/removeadmin", removeAdmin);
11+
12+
export default router;

frontend/src/App.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import NotFound from './pages/NotFound.jsx'
3434
import DevelopersPage from './pages/DevelopersPage.jsx'
3535
import InstallApp from './pages/InstallApp.jsx'
3636
import ScrollToTop from './components/ScrollToTop.jsx'
37+
import MakeAdmin from './pages/MakeAdmin.jsx'
3738

3839
const App = () => {
3940
return (
@@ -87,6 +88,11 @@ const App = () => {
8788
<UserActivityHistory />
8889
</ProtectedRoute>
8990
} />
91+
<Route path='/makeadmin' element={
92+
<ProtectedRoute adminOnly={true}>
93+
<MakeAdmin />
94+
</ProtectedRoute>
95+
} />
9096
<Route path="*" element={<NotFound />} />
9197
</Routes>
9298
</main>

0 commit comments

Comments
 (0)