Skip to content

Commit 16abfd3

Browse files
Make endpoints superadmin only
1 parent 915f40b commit 16abfd3

7 files changed

Lines changed: 137 additions & 51 deletions

File tree

src/services/auth/auth-models.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { z } from "zod";
22

3-
export const Role = z.enum(["USER", "STAFF", "ADMIN", "CORPORATE"]);
3+
export const Role = z.enum([
4+
"USER",
5+
"STAFF",
6+
"ADMIN",
7+
"CORPORATE",
8+
"SUPER_ADMIN",
9+
]);
410
export type Role = z.infer<typeof Role>;
511

612
export enum Platform {

src/services/auth/auth-router.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,38 @@ const oauthClients = {
3535

3636
authRouter.use("/sponsor", authSponsorRouter);
3737

38-
// Remove role from userId (admin only endpoint)
39-
authRouter.delete("/", RoleChecker([Role.Enum.ADMIN]), async (req, res) => {
40-
// Validate request body using Zod schema
41-
const { userId, role } = AuthRoleChangeRequest.parse(req.body);
38+
// Remove role from userId (super admin only endpoint)
39+
authRouter.delete(
40+
"/",
41+
RoleChecker([Role.Enum.SUPER_ADMIN]),
42+
async (req, res) => {
43+
// Validate request body using Zod schema
44+
const { userId, role } = AuthRoleChangeRequest.parse(req.body);
4245

43-
const { data } = await SupabaseDB.AUTH_INFO.select("userId")
44-
.eq("userId", userId)
45-
.maybeSingle()
46-
.throwOnError();
46+
const { data } = await SupabaseDB.AUTH_INFO.select("userId")
47+
.eq("userId", userId)
48+
.maybeSingle()
49+
.throwOnError();
4750

48-
if (!data) {
49-
return res.status(StatusCodes.NOT_FOUND).json({
50-
error: "UserNotFound",
51-
});
52-
}
51+
if (!data) {
52+
return res.status(StatusCodes.NOT_FOUND).json({
53+
error: "UserNotFound",
54+
});
55+
}
5356

54-
const { data: deleted } = await SupabaseDB.AUTH_ROLES.delete()
55-
.eq("userId", userId)
56-
.eq("role", role)
57-
.select()
58-
.single()
59-
.throwOnError();
57+
const { data: deleted } = await SupabaseDB.AUTH_ROLES.delete()
58+
.eq("userId", userId)
59+
.eq("role", role)
60+
.select()
61+
.single()
62+
.throwOnError();
6063

61-
return res.status(StatusCodes.OK).json(deleted);
62-
});
64+
return res.status(StatusCodes.OK).json(deleted);
65+
}
66+
);
6367

64-
// Add role to userId (admin only endpoint)
65-
authRouter.put("/", RoleChecker([Role.Enum.ADMIN]), async (req, res) => {
68+
// Add role to userId (super admin only endpoint)
69+
authRouter.put("/", RoleChecker([Role.Enum.SUPER_ADMIN]), async (req, res) => {
6670
const { userId, role } = AuthRoleChangeRequest.parse(req.body);
6771

6872
const { data } = await SupabaseDB.AUTH_INFO.select("userId")

src/services/notifications/notifications-router.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ jest.mock("../../firebase", () => ({
2222
}),
2323
}));
2424

25-
jest.setTimeout(100000);
26-
2725
function makeTestAttendee(overrides = {}) {
2826
return {
2927
userId: TESTER.userId,
@@ -97,6 +95,10 @@ async function insertTestUser(overrides: InsertTestAttendeeOverrides = {}) {
9795
]).throwOnError();
9896
}
9997

98+
beforeEach(() => {
99+
mockSend.mockReset();
100+
});
101+
100102
describe("/notifications", () => {
101103
describe("POST /notifications/register", () => {
102104
it("should create a notification entry and subscribe to the allUsers topic", async () => {
@@ -141,8 +143,11 @@ describe("/notifications", () => {
141143
});
142144

143145
describe("POST /notifications/topics/:topicName", () => {
144-
it("should send a notification as an admin", async () => {
145-
await post("/notifications/topics/event_123", Role.enum.ADMIN)
146+
it("should send a notification as an super admin", async () => {
147+
const res = await post(
148+
"/notifications/topics/event_123",
149+
Role.enum.SUPER_ADMIN
150+
)
146151
.send({ title: "Admin Test", body: "Admin Message" })
147152
.expect(StatusCodes.OK);
148153

@@ -154,6 +159,23 @@ describe("/notifications", () => {
154159
body: "Admin Message",
155160
},
156161
});
162+
163+
expect(res.body).toMatchObject({
164+
status: "success",
165+
});
166+
});
167+
it("fails if the user is not a super admin", async () => {
168+
const res = await post(
169+
"/notifications/topics/event_123",
170+
Role.enum.ADMIN
171+
)
172+
.send({ title: "Admin Test", body: "Admin Message" })
173+
.expect(StatusCodes.FORBIDDEN);
174+
175+
// Verify Firebase mock was not called
176+
expect(mockSend).not.toHaveBeenCalled();
177+
178+
expect(res.body).toHaveProperty("error", "Forbidden");
157179
});
158180
});
159181

src/services/notifications/notifications-router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ notificationsRouter.post(
3838
}
3939
);
4040

41-
// Admins can send notifications to a specific topic
41+
// Super admins can send notifications to a specific topic
4242
// parameter: the topicName that the admin is sending to
4343
// ^ Can get this from dropdown (will have a route to get all topics)
4444
// Request body: title, body. (title and body of the notification)
4545
notificationsRouter.post(
4646
"/topics/:topicName",
47-
RoleChecker([Role.enum.ADMIN]), // for now thinking that only admins get to use this
47+
RoleChecker([Role.enum.SUPER_ADMIN]),
4848
async (req, res) => {
4949
sendToTopicSchema.parse(req.body); // make sure it fits the validator
5050

src/services/subscription/subscription-router.test.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { describe, expect, it } from "@jest/globals";
2-
import { post, getAsAdmin, postAsAdmin } from "../../../testing/testingTools";
2+
import {
3+
post,
4+
getAsAdmin,
5+
postAsAdmin,
6+
postAsSuperAdmin,
7+
} from "../../../testing/testingTools";
38
import { StatusCodes } from "http-status-codes";
49
import { SupabaseDB } from "../../database";
510
import { IncomingSubscription } from "./subscription-schema";
@@ -188,21 +193,21 @@ describe("GET /subscription/", () => {
188193
});
189194

190195
describe("POST /subscription/send-email", () => {
191-
it("should send an email to all subscribers of a list", async () => {
192-
const mailingList = "test_list";
193-
const subscribers = ["email1@test.com", "email2@test.com"];
196+
const mailingList = "test_list";
197+
const subscribers = ["email1@test.com", "email2@test.com"];
198+
const emailPayload = {
199+
mailingList: mailingList,
200+
subject: "Test Subject",
201+
htmlBody: "<p>Hello World</p>",
202+
};
203+
beforeEach(async () => {
194204
await SupabaseDB.SUBSCRIPTIONS.insert({
195205
mailingList,
196206
subscriptions: subscribers,
197207
});
198-
199-
const emailPayload = {
200-
mailingList: mailingList,
201-
subject: "Test Subject",
202-
htmlBody: "<p>Hello World</p>",
203-
};
204-
205-
await postAsAdmin("/subscription/send-email")
208+
});
209+
it("should send an email to all subscribers of a list", async () => {
210+
const res = await postAsSuperAdmin("/subscription/send-email")
206211
.send(emailPayload)
207212
.expect(StatusCodes.OK);
208213

@@ -222,18 +227,30 @@ describe("POST /subscription/send-email", () => {
222227

223228
// Verify that the send method was actually invoked
224229
expect(mockSESV2Send).toHaveBeenCalledTimes(1);
230+
231+
expect(res.body).toEqual({
232+
status: "success",
233+
});
234+
});
235+
it("fails for a non-super admin", async () => {
236+
const res = await postAsAdmin("/subscription/send-email")
237+
.send(emailPayload)
238+
.expect(StatusCodes.FORBIDDEN);
239+
240+
expect(SendEmailCommand).not.toHaveBeenCalled();
241+
expect(mockSESV2Send).not.toHaveBeenCalled();
242+
expect(res.body).toMatchObject({ error: "Forbidden" });
225243
});
226244
});
227245

228246
describe("POST /subscription/send-email/single", () => {
247+
const emailPayload = {
248+
email: "ritam@test.com",
249+
subject: "Single Email Test",
250+
htmlBody: "<p>Single Email Body</p>",
251+
};
229252
it("should send an email to a single specified email address", async () => {
230-
const emailPayload = {
231-
email: "ritam@test.com",
232-
subject: "Single Email Test",
233-
htmlBody: "<p>Single Email Body</p>",
234-
};
235-
236-
await postAsAdmin("/subscription/send-email/single")
253+
const res = await postAsSuperAdmin("/subscription/send-email/single")
237254
.send(emailPayload)
238255
.expect(StatusCodes.OK);
239256

@@ -252,5 +269,22 @@ describe("POST /subscription/send-email/single", () => {
252269

253270
// Verify that the send method was actually invoked
254271
expect(mockSESV2Send).toHaveBeenCalledTimes(1);
272+
273+
expect(res.body).toEqual({
274+
status: "success",
275+
});
276+
});
277+
278+
it("fails for non super admin", async () => {
279+
const res = await postAsAdmin("/subscription/send-email/single")
280+
.send(emailPayload)
281+
.expect(StatusCodes.FORBIDDEN);
282+
283+
expect(SendEmailCommand).not.toHaveBeenCalled();
284+
285+
// Verify that the send method was actually invoked
286+
expect(mockSESV2Send).not.toHaveBeenCalled();
287+
288+
expect(res.body).toMatchObject({ error: "Forbidden" });
255289
});
256290
});

src/services/subscription/subscription-router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ subscriptionRouter.get(
7676
// API body: {String} mailingList The list to send the email to, {String} subject The subject line of the email, {String} htmlBody The HTML content of the email.
7777
subscriptionRouter.post(
7878
"/send-email",
79-
RoleChecker([Role.Enum.ADMIN]),
79+
RoleChecker([Role.Enum.SUPER_ADMIN]),
8080
async (req, res) => {
8181
const { mailingList, subject, htmlBody } = req.body;
8282
const { data: list } = await SupabaseDB.SUBSCRIPTIONS.select(
@@ -113,7 +113,7 @@ subscriptionRouter.post(
113113
// API body: {String} email (the singular email to send to), {String} subject : The subject line of the email, {String} htmlBody : The HTML content of the email.
114114
subscriptionRouter.post(
115115
"/send-email/single",
116-
RoleChecker([Role.Enum.ADMIN]),
116+
RoleChecker([Role.Enum.SUPER_ADMIN]),
117117
async (req, res) => {
118118
const { email, subject, htmlBody } = req.body;
119119

testing/testingTools.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export function getAsAdmin(url: string): request.Test {
5555
return get(url, Role.enum.ADMIN);
5656
}
5757

58+
export function getAsSuperAdmin(url: string): request.Test {
59+
return get(url, Role.enum.SUPER_ADMIN);
60+
}
61+
5862
export function getAsCorporate(url: string): request.Test {
5963
return get(url, Role.enum.CORPORATE);
6064
}
@@ -86,6 +90,10 @@ export function postAsAdmin(url: string): request.Test {
8690
return post(url, Role.enum.ADMIN);
8791
}
8892

93+
export function postAsSuperAdmin(url: string): request.Test {
94+
return post(url, Role.enum.SUPER_ADMIN);
95+
}
96+
8997
export function postAsCorporate(url: string): request.Test {
9098
return post(url, Role.enum.CORPORATE);
9199
}
@@ -106,6 +114,10 @@ export function putAsAdmin(url: string): request.Test {
106114
return put(url, Role.enum.ADMIN);
107115
}
108116

117+
export function putAsSuperAdmin(url: string): request.Test {
118+
return put(url, Role.enum.SUPER_ADMIN);
119+
}
120+
109121
export function putAsCorporate(url: string): request.Test {
110122
return put(url, Role.enum.CORPORATE);
111123
}
@@ -126,6 +138,10 @@ export function patchAsAdmin(url: string): request.Test {
126138
return patch(url, Role.enum.ADMIN);
127139
}
128140

141+
export function patchAsSuperAdmin(url: string): request.Test {
142+
return patch(url, Role.enum.SUPER_ADMIN);
143+
}
144+
129145
export function patchAsCorporate(url: string): request.Test {
130146
return patch(url, Role.enum.CORPORATE);
131147
}
@@ -146,6 +162,10 @@ export function delAsAdmin(url: string): request.Test {
146162
return del(url, Role.enum.ADMIN);
147163
}
148164

165+
export function delAsSuperAdmin(url: string): request.Test {
166+
return del(url, Role.enum.SUPER_ADMIN);
167+
}
168+
149169
export function delAsCorporate(url: string): request.Test {
150170
return del(url, Role.enum.CORPORATE);
151171
}

0 commit comments

Comments
 (0)