Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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, "higher")) {
/* 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),
});
if (!role) throw new Error("Role not found");

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

throw new Error(
"You do not have permission to edit this role.",
);
}
if (
position !== undefined &&
!compareUserPosition(user, position, "higher")
) {
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, "higher")) {
/* 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