Skip to content

Commit 4341eae

Browse files
committed
Add centralized auth guards to server actions
- New src/lib/utils/auth.ts with getSessionUser, requireUser, isAdmin, requireAdmin - Gate all admin server actions (getForms, getAllFormDetails, getSubmissions) - Fix otherUser escalation: only admins may act on behalf of other users - Move form reads out of admin actions into plain server module so member-facing pages stop importing from the admin RPC surface
1 parent 1219acc commit 4341eae

8 files changed

Lines changed: 87 additions & 60 deletions

File tree

‎src/app/portal/admin/actions.ts‎

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use server";
22
import { db } from "@/db";
3-
import { createClient } from "@/lib/utils/supabase/server";
3+
import { requireAdmin } from "@/lib/utils/auth";
4+
import { getFormById } from "@/lib/utils/forms/server";
45
import { FormStep } from "@/lib/types/questions";
56
import { FormFields } from "@/components/forms/applications/columns";
67
import { sendEmail } from "@/lib/utils/forms/email";
@@ -9,41 +10,15 @@ import { render } from "@react-email/components";
910
import { object } from "zod";
1011

1112
export async function getForms() {
13+
await requireAdmin();
1214
return db.forms.findMany();
1315
}
1416

15-
async function requireAdmin() {
16-
const supabase = createClient();
17-
const { data, error } = await supabase.auth.getUser();
18-
if (error || !data.user) {
19-
throw new Error("Unauthorized");
20-
}
21-
const role = await db.roles.findUnique({
22-
where: { id: data.user.id },
23-
});
24-
if (!role || role.roles?.split(",")[0] !== "admin") {
25-
throw new Error("Unauthorized");
26-
}
27-
}
28-
2917
export async function createForm(data: { title: string; description: string }) {
3018
await requireAdmin();
3119
return db.forms.create({ data: { ...data, config: {}, questions: [] } });
3220
}
3321

34-
export async function getForm(id: number | bigint) {
35-
try {
36-
return db.forms.findFirst({
37-
where: {
38-
id: BigInt(id),
39-
},
40-
});
41-
} catch (e) {
42-
console.log(e);
43-
return null;
44-
}
45-
}
46-
4722
export async function updateForm(
4823
id: number,
4924
data: {
@@ -62,6 +37,7 @@ export async function getSubmissions(
6237
onlySubmitted: boolean = true,
6338
filters: any | undefined = {},
6439
) {
40+
await requireAdmin();
6541
const app = await db.submissions.findMany({
6642
include: {
6743
users: true,
@@ -116,6 +92,7 @@ function formatFormFields(questionSteps: FormStep[]): FormFields {
11692
export async function getAllFormDetails(
11793
formId: bigint,
11894
): Promise<{ rawForm: any; formFields: FormFields; submissions: any[] }> {
95+
await requireAdmin();
11996
try {
12097
const form = await db.forms.findFirst({
12198
where: { id: formId },
@@ -268,7 +245,7 @@ async function sendStatusEmail({
268245
formId: bigint;
269246
userId: string;
270247
}) {
271-
const form = await getForm(formId);
248+
const form = await getFormById(formId);
272249

273250
if (!form) {
274251
console.log("Form not found");

‎src/app/portal/admin/forms/[id]/settings/actions.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use server";
22
import { db } from "@/db";
3+
import { requireAdmin } from "@/lib/utils/auth";
34

45
type EmailTemplate = {
56
title: string;
@@ -11,6 +12,7 @@ export async function updateOrCreateEmailTemplate(
1112
status: string,
1213
template: EmailTemplate,
1314
) {
15+
await requireAdmin();
1416
try {
1517
// First get the current form to access existing config
1618
const form = await db.forms.findFirst({

‎src/app/portal/forms/[id]/application/page.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { redirect } from "next/navigation";
22
import { GenericResult, AcceptedResult } from "@/components/forms/resultPages";
3-
import { getForm } from "@/app/portal/admin/actions";
3+
import { getFormById } from "@/lib/utils/forms/server";
44
import { getUserApplication } from "@/app/portal/forms/actions";
55
import { Form } from "@/lib/types/application";
66
import GenericGreeter from "@/components/layouts/genericGreeter";
@@ -22,7 +22,7 @@ export default async function page({
2222
if (!params.id) {
2323
redirect("/portal/forms");
2424
}
25-
const formP = getForm(Number(params.id)) as unknown as Promise<Form>;
25+
const formP = getFormById(Number(params.id)) as unknown as Promise<Form>;
2626
const appP = getUserApplication({
2727
formId: Number(params.id) as unknown as bigint,
2828
includeApp: true,

‎src/app/portal/forms/[id]/apply/page.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getUserApplication } from "../../actions";
22
import { redirect } from "next/navigation";
33
import ApplicationForm from "@/components/forms/applications/applicationForm";
44
import { Application } from "@/lib/types/questions";
5-
import { getForm } from "@/app/portal/admin/actions";
5+
import { getFormById } from "@/lib/utils/forms/server";
66
import { Form } from "@/lib/types/application";
77
import { isFormOpen } from "@/lib/utils/forms/helpers";
88
import { toast } from "sonner";
@@ -13,7 +13,7 @@ export default async function page({
1313
params: { id: string };
1414
searchParams: { [key: string]: string | string[] | undefined };
1515
}) {
16-
const form = (await getForm(Number(params.id))) as unknown as Form;
16+
const form = (await getFormById(Number(params.id))) as unknown as Form;
1717

1818
if (!form) {
1919
return redirect(`/portal`);

‎src/app/portal/forms/[id]/page.tsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
// page.tsx
21
import { redirect } from "next/navigation";
32
import { isFormOpen } from "@/lib/utils/forms/helpers";
4-
import { getForm } from "@/app/portal/admin/actions";
3+
import { getFormById } from "@/lib/utils/forms/server";
54
import { getUserApplication } from "@/app/portal/forms/actions";
65
import { Form } from "@/lib/types/application";
76
import GenericGreeter from "@/components/layouts/genericGreeter";
87
import { MainResultPage } from "@/components/forms/resultPages/MainResultPage";
98

109
async function getPageData(id: string) {
11-
const formP = getForm(Number(id)) as unknown as Promise<Form>;
10+
const formP = getFormById(Number(id)) as unknown as Promise<Form>;
1211
const appP = getUserApplication({
1312
formId: Number(id) as unknown as bigint,
1413
});

‎src/app/portal/forms/actions.ts‎

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render } from "@react-email/components";
33
import { db } from "@/db";
44
import { FormStep, Obj } from "@/lib/types/questions";
55
import { Trigger } from "@/lib/types/forms";
6-
import { createClient } from "@/lib/utils/supabase/server";
6+
import { getSessionUser, requireUser, isAdmin } from "@/lib/utils/auth";
77
import { JSONValidationToZod } from "@/lib/utils/forms/helpers";
88
import { sendEmail } from "@/lib/utils/forms/email";
99
import { SubmissionTemplate } from "@/components/forms/emailTemplates/submissionTemplate";
@@ -15,12 +15,13 @@ export async function submitApplication({
1515
formId: bigint;
1616
otherUser?: string;
1717
}) {
18-
const supabase = createClient();
19-
const userId = otherUser || (await supabase.auth.getUser()).data?.user?.id;
20-
const { data, error } = await supabase.auth.getUser();
21-
if (!data.user || error) {
22-
return null;
18+
const user = await requireUser();
19+
// otherUser lets admins submit on behalf of an applicant (support flow);
20+
// regular users may only ever act as themselves.
21+
if (otherUser && !(await isAdmin(user.id))) {
22+
throw new Error("Unauthorized");
2323
}
24+
const userId = otherUser ? otherUser : user.id;
2425
const resPromise = db.submissions.findUnique({
2526
where: {
2627
user_id_form_id: {
@@ -121,7 +122,7 @@ export async function submitApplication({
121122
await sendEmail({
122123
from: "no-reply@ubclaunchpad.com",
123124
fromName: "No-reply UBC Launch Pad",
124-
to: data.user.email!.toString(),
125+
to: user.email!.toString(),
125126
subject: `${form.title} - Form Submitted`,
126127
html: template,
127128
cc: appEmail?.email as string,
@@ -138,12 +139,11 @@ export async function updateApplication({
138139
formId: bigint;
139140
otherUser?: string;
140141
}) {
141-
const supabase = createClient();
142-
const userId = otherUser || (await supabase.auth.getUser()).data?.user?.id;
143-
const { data, error } = await supabase.auth.getUser();
144-
if (!data.user || error) {
145-
return null;
142+
const user = await requireUser();
143+
if (otherUser && !(await isAdmin(user.id))) {
144+
throw new Error("Unauthorized");
146145
}
146+
const userId = otherUser ? otherUser : user.id;
147147

148148
const res = await db.submissions.findUnique({
149149
where: {
@@ -176,16 +176,15 @@ export async function getUserApplication({
176176
formId: bigint;
177177
includeApp?: boolean;
178178
}) {
179-
const supabase = createClient();
180-
const { data, error } = await supabase.auth.getUser();
181-
if (!data.user || error) {
179+
const user = await getSessionUser();
180+
if (!user) {
182181
return null;
183182
}
184183
return db.submissions.findUnique({
185184
where: {
186185
user_id_form_id: {
187186
form_id: formId,
188-
user_id: data.user.id,
187+
user_id: user.id,
189188
},
190189
},
191190
include: {
@@ -219,16 +218,12 @@ function validateFormAnswers({
219218
}
220219

221220
export async function startApplication({ formId }: { formId: bigint }) {
222-
const supabase = createClient();
223-
const { data, error } = await supabase.auth.getUser();
224-
if (!data.user || error) {
225-
return null;
226-
}
221+
const user = await requireUser();
227222
const res = await db.submissions.findUnique({
228223
where: {
229224
user_id_form_id: {
230225
form_id: formId,
231-
user_id: data.user.id,
226+
user_id: user.id,
232227
},
233228
},
234229
});
@@ -240,7 +235,7 @@ export async function startApplication({ formId }: { formId: bigint }) {
240235
where: {
241236
user_id_form_id: {
242237
form_id: formId,
243-
user_id: data.user.id,
238+
user_id: user.id,
244239
},
245240
},
246241
data: {
@@ -250,7 +245,7 @@ export async function startApplication({ formId }: { formId: bigint }) {
250245
}
251246
return db.submissions.create({
252247
data: {
253-
user_id: data.user.id,
248+
user_id: user.id,
254249
form_id: formId,
255250
status: "pending",
256251
},

‎src/lib/utils/auth.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { createClient } from "@/lib/utils/supabase/server";
2+
import { db } from "@/db";
3+
import { User } from "@supabase/supabase-js";
4+
5+
export async function getSessionUser(): Promise<User | null> {
6+
const supabase = createClient();
7+
const { data, error } = await supabase.auth.getUser();
8+
if (error || !data.user) {
9+
return null;
10+
}
11+
return data.user;
12+
}
13+
14+
export async function isAdmin(userId: string): Promise<boolean> {
15+
const role = await db.roles.findUnique({
16+
where: { id: userId },
17+
});
18+
return !!role && role.roles?.split(",")[0] === "admin";
19+
}
20+
21+
export async function requireUser(): Promise<User> {
22+
const user = await getSessionUser();
23+
if (!user) {
24+
throw new Error("Unauthorized");
25+
}
26+
return user;
27+
}
28+
29+
export async function requireAdmin() {
30+
const user = await getSessionUser();
31+
if (!user) {
32+
throw new Error("Unauthorized");
33+
}
34+
if (!(await isAdmin(user.id))) {
35+
throw new Error("Unauthorized");
36+
}
37+
return user;
38+
}

‎src/lib/utils/forms/server.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { db } from "@/db";
2+
3+
// Plain server module (no "use server") — internal reads shared by
4+
// member-facing pages and admin flows. Not an invokable RPC endpoint.
5+
export async function getFormById(id: number | bigint) {
6+
try {
7+
return await db.forms.findFirst({
8+
where: {
9+
id: BigInt(id),
10+
},
11+
});
12+
} catch (e) {
13+
console.log(e);
14+
return null;
15+
}
16+
}

0 commit comments

Comments
 (0)