Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# dependencies
node_modules
.pnp
.pnp*
.pnp.js

# testing
Expand Down
1 change: 0 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
"tailwind-merge": "^2.4.0",
"tailwindcss": "3.4.6",
"tailwindcss-animate": "^1.0.7",
"title-case": "^4.3.1",
"typescript": "5.5.3",
"use-debounce": "^10.0.1",
"usehooks-ts": "^3.1.0",
Expand Down
35 changes: 29 additions & 6 deletions apps/web/src/actions/admin/event-actions.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
"use server";

import { PermissionType } from "@/lib/constants/permission";
import { adminAction } from "@/lib/safe-action";
import { newEventFormSchema as editEventFormSchema } from "@/validators/event";
import { editEvent as modifyEvent } from "db/functions";
import { userHasPermission } from "@/lib/utils/server/admin";
import { newEventFormSchema, editEventFormSchema } from "@/validators/event";
import { createNewEvent, editEvent as modifyEvent } from "db/functions";
import { deleteEvent as removeEvent } from "db/functions";
import { revalidatePath } from "next/cache";
import { z } from "zod";

export const editEvent = adminAction
.schema(editEventFormSchema)
.action(async ({ parsedInput }) => {
const { id, ...options } = parsedInput;

.action(async ({ parsedInput: { id, ...options }, ctx: { user } }) => {
if (id === undefined) {
throw new Error("The event's ID is not defined");
}

if (!userHasPermission(user, PermissionType.EDIT_EVENTS)) {
throw new Error("You do not have permission to edit events.");
}

try {
await modifyEvent(id, options);
revalidatePath("/admin/events");
Expand All @@ -29,9 +33,28 @@ export const editEvent = adminAction
}
});

export const createEvent = adminAction
.schema(newEventFormSchema)
.action(async ({ parsedInput, ctx: { user } }) => {
if (!userHasPermission(user, PermissionType.CREATE_EVENTS)) {
throw new Error("You do not have permission to create events.");
}

const res = await createNewEvent(parsedInput);
return {
success: true,
message: "Event created successfully.",
redirect: `/schedule/${res[0].eventID}`,
};
});

export const deleteEventAction = adminAction
.schema(z.object({ eventID: z.number().positive().int() }))
.action(async ({ parsedInput }) => {
.action(async ({ parsedInput, ctx: { user } }) => {
if (!userHasPermission(user, PermissionType.DELETE_EVENTS)) {
throw new Error("You do not have permission to delete events.");
}

await removeEvent(parsedInput.eventID);
revalidatePath("/admin/events");
return { success: true };
Expand Down
59 changes: 44 additions & 15 deletions apps/web/src/actions/admin/modify-nav-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { redisSAdd, redisHSet, removeNavItem } from "@/lib/utils/server/redis";
import { revalidatePath } from "next/cache";

import { Redis } from "@upstash/redis";
import { userHasPermission } from "@/lib/utils/server/admin";
import { PermissionType } from "@/lib/constants/permission";

const redis = Redis.fromEnv();

Expand All @@ -25,6 +27,12 @@ const navAdminPage = "/admin/toggles/landing";
export const setItem = adminAction
.schema(metadataSchema)
.action(async ({ parsedInput: { name, url }, ctx: { user, userId } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) {
throw new Error(
"You do not have permission to manage navigation links.",
);
}

await redisSAdd("config:navitemslist", encodeURIComponent(name));
await redisHSet(`config:navitems:${encodeURIComponent(name)}`, {
url,
Expand All @@ -37,29 +45,45 @@ export const setItem = adminAction

export const editItem = adminAction
.schema(editMetadataSchema)
.action(async ({ parsedInput: { name, url, existingName } }) => {
const pipe = redis.pipeline();
.action(
async ({ parsedInput: { name, url, existingName }, ctx: { user } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) {
throw new Error(
"You do not have permission to manage navigation links.",
);
}

if (existingName != name) {
pipe.srem("config:navitemslist", encodeURIComponent(existingName));
}
const pipe = redis.pipeline();

pipe.sadd("config:navitemslist", encodeURIComponent(name));
pipe.hset(`config:navitems:${encodeURIComponent(name)}`, {
url,
name,
enabled: true,
});
if (existingName != name) {
pipe.srem(
"config:navitemslist",
encodeURIComponent(existingName),
);
}

await pipe.exec();
pipe.sadd("config:navitemslist", encodeURIComponent(name));
pipe.hset(`config:navitems:${encodeURIComponent(name)}`, {
url,
name,
enabled: true,
});

revalidatePath(navAdminPage);
return { success: true };
});
await pipe.exec();

revalidatePath(navAdminPage);
return { success: true };
},
);

export const removeItem = adminAction
.schema(z.string())
.action(async ({ parsedInput: name, ctx: { user, userId } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) {
throw new Error(
"You do not have permission to manage navigation links.",
);
}
await removeNavItem(name);
// await new Promise((resolve) => setTimeout(resolve, 1500));
revalidatePath(navAdminPage);
Expand All @@ -73,6 +97,11 @@ export const toggleItem = adminAction
parsedInput: { name, statusToSet },
ctx: { user, userId },
}) => {
if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) {
throw new Error(
"You do not have permission to manage navigation links.",
);
}
await redisHSet(`config:navitems:${encodeURIComponent(name)}`, {
enabled: statusToSet,
});
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/actions/admin/registration-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { z } from "zod";
import { adminAction } from "@/lib/safe-action";
import { redisSet } from "@/lib/utils/server/redis";
import { revalidatePath } from "next/cache";
import { userHasPermission } from "@/lib/utils/server/admin";
import { PermissionType } from "@/lib/constants/permission";

const defaultRegistrationToggleSchema = z.object({
enabled: z.boolean(),
Expand All @@ -16,6 +18,12 @@ const defaultRSVPLimitSchema = z.object({
export const toggleRegistrationEnabled = adminAction
.schema(defaultRegistrationToggleSchema)
.action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) {
throw new Error(
"You do not have permission to manage registration settings.",
);
}

await redisSet("config:registration:registrationEnabled", enabled);
revalidatePath("/admin/toggles/registration");
return { success: true, statusSet: enabled };
Expand All @@ -24,6 +32,12 @@ export const toggleRegistrationEnabled = adminAction
export const toggleRegistrationMessageEnabled = adminAction
.schema(defaultRegistrationToggleSchema)
.action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) {
throw new Error(
"You do not have permission to manage registration settings.",
);
}

await redisSet(
"config:registration:registrationMessageEnabled",
enabled,
Expand All @@ -35,6 +49,12 @@ export const toggleRegistrationMessageEnabled = adminAction
export const toggleRSVPs = adminAction
.schema(defaultRegistrationToggleSchema)
.action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) {
throw new Error(
"You do not have permission to manage registration settings.",
);
}

await redisSet("config:registration:allowRSVPs", enabled);
revalidatePath("/admin/toggles/registration");
return { success: true, statusSet: enabled };
Expand All @@ -43,6 +63,12 @@ export const toggleRSVPs = adminAction
export const setRSVPLimit = adminAction
.schema(defaultRSVPLimitSchema)
.action(async ({ parsedInput: { rsvpLimit }, ctx: { user, userId } }) => {
if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) {
throw new Error(
"You do not have permission to manage registration settings.",
);
}

await redisSet("config:registration:maxRSVPs", rsvpLimit);
revalidatePath("/admin/toggles/registration");
return { success: true, statusSet: rsvpLimit };
Expand Down
139 changes: 139 additions & 0 deletions apps/web/src/actions/admin/role-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use server";

import { z } from "zod";
import { adminAction } from "@/lib/safe-action";
import { db } from "db";
import { roles, userCommonData } from "db/schema";
import { eq } from "db/drizzle";
import { revalidatePath } from "next/cache";
import { PermissionType } from "@/lib/constants/permission";
import {
userHasPermission,
compareUserPosition,
} from "@/lib/utils/server/admin";

const createRoleSchema = z.object({
name: z.string().min(1).max(50),
position: z.number().int().nonnegative(),
permissions: z.number().nonnegative(),
color: z.string().optional(),
});

const editRoleSchema = z.object({
roleId: z.number().int().positive(),
name: z.string().min(1).max(50).optional(),
position: z.number().int().nonnegative().optional(),
permissions: z.number().optional(),
color: z.string().optional(),
});

const deleteRoleSchema = z.object({
roleId: z.number().int().positive(),
});

export const createRole = adminAction
.schema(createRoleSchema)
.action(
async ({
parsedInput: { name, position, permissions, color },
ctx: { user },
}) => {
if (!userHasPermission(user, PermissionType.CREATE_ROLES)) {
if (!compareUserPosition(user, position)) {
/* This prevents creation of roles higher-or-equal to the current user's position */

throw new Error(
"You do not have permission to create a role at this position.",
);
}
}

const existing = await db.query.roles.findFirst({
where: eq(roles.name, name),
});
if (existing)
throw new Error("Role with that name already exists.");

await db
.insert(roles)
.values({ name, position, permissions, color });
revalidatePath("/admin/roles");
return { success: true };
},
);

export const editRole = adminAction
.schema(editRoleSchema)
.action(
async ({
parsedInput: { roleId, name, position, permissions, color },
ctx: { user },
}) => {
const role = await db.query.roles.findFirst({
where: eq(roles.id, roleId),
});
console.log(user);
if (!role) throw new Error("Role not found");

if (
!userHasPermission(user, PermissionType.EDIT_ROLES) ||
!compareUserPosition(user, role.position)
) {
throw new Error(
"You do not have permission to edit this role.",
);
}
if (
position !== undefined &&
!compareUserPosition(user, position)
) {
throw new Error(
"You do not have permission to move a role to that position.",
);
}

await db
.update(roles)
.set({
name: name ?? role.name,
position: position ?? role.position,
permissions: permissions ?? role.permissions,
color: color ?? role.color,
})
.where(eq(roles.id, roleId));

revalidatePath("/admin/roles");
return { success: true };
},
);

export const deleteRole = adminAction
.schema(deleteRoleSchema)
.action(async ({ parsedInput: { roleId }, ctx: { user } }) => {
const role = await db.query.roles.findFirst({
where: eq(roles.id, roleId),
});
if (!role) throw new Error("Role not found");

const userCount = await db.query.userCommonData.findMany({
where: eq(userCommonData.role_id, roleId),
});

if (userCount.length > 0) {
throw new Error("Cannot delete a role that is assigned to users.");
}

if (!userHasPermission(user, PermissionType.DELETE_ROLES)) {
if (!compareUserPosition(user, role.position)) {
/* This prevents deletion of roles higher-or-equal to the current user's position */

throw new Error(
"You do not have permission to delete this role.",
);
}
}

await db.delete(roles).where(eq(roles.id, roleId));
revalidatePath("/admin/roles");
return { success: true };
});
Loading