diff --git a/apps/infrastructure-migrator/driver.ts b/apps/infrastructure-migrator/driver.ts
index ffbab1e7..012bdc1e 100644
--- a/apps/infrastructure-migrator/driver.ts
+++ b/apps/infrastructure-migrator/driver.ts
@@ -240,7 +240,7 @@ async function migratePostgresSqLite() {
const cmd = new PutObjectCommand({
Key: key,
- Bucket: staticUploads.bucketName,
+ Bucket: process.env.R2_BUCKET_NAME,
ContentType: "application/pdf",
///@ts-expect-error
Body: buffer,
diff --git a/apps/web/src/actions/admin/event-actions.ts b/apps/web/src/actions/admin/event-actions.ts
index 34f9dbf6..24ee4b71 100644
--- a/apps/web/src/actions/admin/event-actions.ts
+++ b/apps/web/src/actions/admin/event-actions.ts
@@ -1,6 +1,6 @@
"use server";
-import { adminAction, superAdminAction } from "@/lib/safe-action";
+import { adminAction } from "@/lib/safe-action";
import { newEventFormSchema as editEventFormSchema } from "@/validators/event";
import { editEvent as modifyEvent } from "db/functions";
import { deleteEvent as removeEvent } from "db/functions";
diff --git a/apps/web/src/actions/admin/scanner-admin-actions.ts b/apps/web/src/actions/admin/scanner-admin-actions.ts
index 60f1aeed..3447db0d 100644
--- a/apps/web/src/actions/admin/scanner-admin-actions.ts
+++ b/apps/web/src/actions/admin/scanner-admin-actions.ts
@@ -1,12 +1,12 @@
"use server";
-import { adminAction } from "@/lib/safe-action";
+import { volunteerAction } from "@/lib/safe-action";
import { z } from "zod";
import { db, sql } from "db";
import { scans, userCommonData } from "db/schema";
import { eq, and } from "db/drizzle";
-export const createScan = adminAction
+export const createScan = volunteerAction
.schema(
z.object({
eventID: z.number(),
@@ -49,7 +49,7 @@ export const createScan = adminAction
},
);
-export const getScan = adminAction
+export const getScan = volunteerAction
.schema(z.object({ eventID: z.number(), userID: z.string() }))
.action(
async ({
@@ -77,7 +77,7 @@ const checkInUserSchema = z.object({
}, "QR Code has expired. Please tell user refresh the QR Code"),
});
-export const checkInUserToHackathon = adminAction
+export const checkInUserToHackathon = volunteerAction
.schema(checkInUserSchema)
.action(async ({ parsedInput: { userID } }) => {
// Set checkinTimestamp
diff --git a/apps/web/src/actions/admin/user-actions.ts b/apps/web/src/actions/admin/user-actions.ts
index 41b46656..919a9320 100644
--- a/apps/web/src/actions/admin/user-actions.ts
+++ b/apps/web/src/actions/admin/user-actions.ts
@@ -23,7 +23,9 @@ export const updateRole = adminAction
}) => {
if (
user.role !== "super_admin" &&
- (roleToSet === "super_admin" || roleToSet === "admin")
+ (roleToSet === "super_admin" ||
+ roleToSet === "admin" ||
+ roleToSet === "volunteer")
) {
returnValidationErrors(z.null(), {
_errors: ["You are not allowed to do this!"],
diff --git a/apps/web/src/app/admin/events/page.tsx b/apps/web/src/app/admin/events/page.tsx
index f95b1612..d43e3b4d 100644
--- a/apps/web/src/app/admin/events/page.tsx
+++ b/apps/web/src/app/admin/events/page.tsx
@@ -7,6 +7,8 @@ import { PlusCircle } from "lucide-react";
import Link from "next/link";
import { getAllEvents, getUser } from "db/functions";
import { auth } from "@clerk/nextjs/server";
+import FullScreenMessage from "@/components/shared/FullScreenMessage";
+import { isUserAdmin } from "@/lib/utils/server/admin";
export default async function Page() {
const { userId, redirectToSignIn } = await auth();
@@ -15,10 +17,17 @@ export default async function Page() {
}
const userData = await getUser(userId);
- const isSuperAdmin = userData?.role === "super_admin";
+ if (!userData) {
+ return (
+
+ );
+ }
const events = await getAllEvents();
-
+ const isUserAuthorized = isUserAdmin(userData);
return (
@@ -32,18 +41,23 @@ export default async function Page() {
-
+ {isUserAuthorized && (
+
+ )}
({ ...ev, isSuperAdmin }))}
+ data={events.map((ev) => ({
+ ...ev,
+ isUserAdmin: isUserAuthorized,
+ }))}
/>
);
diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx
index 78698ce1..47cd9a1f 100644
--- a/apps/web/src/app/admin/layout.tsx
+++ b/apps/web/src/app/admin/layout.tsx
@@ -24,7 +24,12 @@ export default async function AdminLayout({ children }: AdminLayoutProps) {
const user = await getUser(userId);
- if (!user || (user.role !== "admin" && user.role !== "super_admin")) {
+ if (
+ !user ||
+ (user.role !== "admin" &&
+ user.role !== "super_admin" &&
+ user.role !== "volunteer")
+ ) {
console.log("Denying admin access to user", user);
return (
- {Object.entries(c.dashPaths.admin).map(([name, path]) => (
-
- ))}
+ {Object.entries(c.dashPaths.admin).map(([name, path]) =>
+ ["Users", "Toggles"].includes(name) &&
+ user.role === "volunteer" ? null : (
+
+ ),
+ )}
Loading...}>{children}
>
diff --git a/apps/web/src/app/admin/page.tsx b/apps/web/src/app/admin/page.tsx
index e7d76242..39fb7b45 100644
--- a/apps/web/src/app/admin/page.tsx
+++ b/apps/web/src/app/admin/page.tsx
@@ -11,6 +11,10 @@ import type { User } from "db/types";
import { auth } from "@clerk/nextjs/server";
import { notFound } from "next/navigation";
import { getAllUsers, getUser } from "db/functions";
+import Link from "next/link";
+import { getRequestContext } from "@cloudflare/next-on-pages";
+import { formatInTimeZone } from "date-fns-tz";
+import { getClientTimeZone } from "@/lib/utils/client/shared";
export default async function Page() {
const { userId } = await auth();
@@ -19,15 +23,24 @@ export default async function Page() {
const adminUser = await getUser(userId);
if (
!adminUser ||
- (adminUser.role !== "admin" && adminUser.role !== "super_admin")
+ (adminUser.role !== "admin" &&
+ adminUser.role !== "super_admin" &&
+ adminUser.role !== "volunteer")
) {
return notFound();
}
const allUsers = (await getAllUsers()) ?? [];
- const { rsvpCount, checkinCount, recentSignupCount } =
- getRecentRegistrationData(allUsers);
+ const {
+ rsvpCount,
+ checkinCount,
+ recentSignupCount,
+ recentRegisteredUsers,
+ } = getRecentRegistrationData(allUsers);
+ const { cf } = getRequestContext();
+
+ const timezone = getClientTimeZone(cf.timezone);
return (
@@ -49,7 +62,6 @@ export default async function Page() {
{allUsers.length}
- {/*
+20.1% from last month
*/}
@@ -61,7 +73,6 @@ export default async function Page() {
{0}
- {/* +20.1% from last month
*/}
@@ -73,7 +84,6 @@ export default async function Page() {
{rsvpCount}
- {/* +20.1% from last month
*/}
@@ -85,7 +95,6 @@ export default async function Page() {
{checkinCount}
- {/* +20.1% from last month
*/}
@@ -105,7 +114,6 @@ export default async function Page() {
days.
-
@@ -119,10 +127,32 @@ export default async function Page() {
Recent Registrations
{" "}
-
-
+
+
+ {recentRegisteredUsers.map((user) => (
+
+
+ {user.firstName} {user.lastName}
+
+
+ {formatInTimeZone(
+ user.signupTime,
+ timezone,
+ "MMMM dd h:mm a",
+ )}
+
+
+ ))}
+
+
@@ -134,6 +164,9 @@ function getRecentRegistrationData(users: User[]) {
let rsvpCount = 0;
let checkinCount = 0;
+
+ const recentRegisteredUsers: User[] = [];
+ let recentRegisteredUsersCount = 0;
let recentSignupCount: DateNumberMap = {};
for (let i = 0; i < 7; i++) {
@@ -154,10 +187,21 @@ function getRecentRegistrationData(users: User[]) {
const stamp = user.signupTime.toISOString().split("T")[0];
- if (recentSignupCount[stamp] != undefined) recentSignupCount[stamp]++;
+ if (recentSignupCount[stamp] != undefined) {
+ if (recentRegisteredUsersCount < 10) {
+ recentRegisteredUsers.push(user);
+ recentRegisteredUsersCount++;
+ }
+ recentSignupCount[stamp]++;
+ }
}
- return { rsvpCount, checkinCount, recentSignupCount };
+ return {
+ rsvpCount,
+ checkinCount,
+ recentSignupCount,
+ recentRegisteredUsers,
+ };
}
export const runtime = "edge";
diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx
index a800519d..6e762102 100644
--- a/apps/web/src/app/admin/users/page.tsx
+++ b/apps/web/src/app/admin/users/page.tsx
@@ -5,9 +5,20 @@ import { Button } from "@/components/shadcn/ui/button";
import { FolderInput } from "lucide-react";
import { getAllUsers } from "db/functions";
import { userCommonData } from "db/schema";
+import { getUser } from "db/functions";
+import { auth } from "@clerk/nextjs/server";
+import { notFound } from "next/navigation";
+import { isUserAdmin } from "@/lib/utils/server/admin";
// This begs a question where we might want to have an option later on to sort by the role as we might want different things
export default async function Page() {
+ const { userId } = await auth();
+
+ if (!userId) return notFound();
+
+ const admin = await getUser(userId);
+ if (!admin || !isUserAdmin(admin)) return notFound();
+
const userData = await getAllUsers();
return (
diff --git a/apps/web/src/app/api/upload/pfp/route.ts b/apps/web/src/app/api/upload/pfp/route.ts
index 194633a3..187c34ae 100644
--- a/apps/web/src/app/api/upload/pfp/route.ts
+++ b/apps/web/src/app/api/upload/pfp/route.ts
@@ -26,7 +26,10 @@ export async function POST(request: Request): Promise {
const randomSeq = crypto.randomUUID();
const [fileName, extension] = body.fileName.split(".");
const key = `${body.location}/${fileName}-${randomSeq}.${extension}`;
- const url = await getPresignedUploadUrl(staticUploads.bucketName, key);
+ const url = await getPresignedUploadUrl(
+ process.env.R2_BUCKET_NAME!,
+ key,
+ );
return NextResponse.json({ url, key });
} catch (error) {
diff --git a/apps/web/src/app/api/upload/resume/register/route.ts b/apps/web/src/app/api/upload/resume/register/route.ts
index 97957372..b05e53e6 100644
--- a/apps/web/src/app/api/upload/resume/register/route.ts
+++ b/apps/web/src/app/api/upload/resume/register/route.ts
@@ -25,7 +25,10 @@ export async function POST(request: Request): Promise {
const randomSeq = crypto.randomUUID();
const [fileName, extension] = body.fileName.split(".");
const key = `${body.location}/${fileName}-${randomSeq}.${extension}`;
- const url = await getPresignedUploadUrl(staticUploads.bucketName, key);
+ const url = await getPresignedUploadUrl(
+ process.env.R2_BUCKET_NAME!,
+ key,
+ );
return NextResponse.json({ url, key });
} catch (error) {
diff --git a/apps/web/src/app/api/upload/resume/view/route.ts b/apps/web/src/app/api/upload/resume/view/route.ts
index 26597166..5e54bf21 100644
--- a/apps/web/src/app/api/upload/resume/view/route.ts
+++ b/apps/web/src/app/api/upload/resume/view/route.ts
@@ -25,7 +25,7 @@ export async function GET(request: Request) {
// Presign the url and return redirect to it.
const presignedViewingUrl = await getPresignedViewingUrl(
- staticUploads.bucketName,
+ process.env.R2_BUCKET_NAME!,
decodedKey,
);
diff --git a/apps/web/src/components/admin/toggles/RegistrationSettings.tsx b/apps/web/src/components/admin/toggles/RegistrationSettings.tsx
index 0b415fd6..bbf699ad 100644
--- a/apps/web/src/components/admin/toggles/RegistrationSettings.tsx
+++ b/apps/web/src/components/admin/toggles/RegistrationSettings.tsx
@@ -93,7 +93,8 @@ export function RegistrationToggles({
}}
/>
-
+ {/* removed until implemented */}
+ {/*
Allow Secret Code Sign-up
@@ -111,7 +112,7 @@ export function RegistrationToggles({
});
}}
/>
-
+
*/}
diff --git a/apps/web/src/components/events/shared/EventColumns.tsx b/apps/web/src/components/events/shared/EventColumns.tsx
index 68061444..fb6ed5a6 100644
--- a/apps/web/src/components/events/shared/EventColumns.tsx
+++ b/apps/web/src/components/events/shared/EventColumns.tsx
@@ -31,8 +31,9 @@ import { useAction } from "next-safe-action/hooks";
import { deleteEventAction } from "@/actions/admin/event-actions";
import { toast } from "sonner";
import { LoaderCircle } from "lucide-react";
+import { error } from "console";
-type EventRow = eventTableValidatorType & { isSuperAdmin: boolean };
+type EventRow = eventTableValidatorType & { isUserAdmin: boolean };
export const columns: ColumnDef
[] = [
{
@@ -104,7 +105,18 @@ export const columns: ColumnDef[] = [
router.refresh();
setOpen(false);
},
- onError: (err) => {
+ onError: ({ error: err }) => {
+ let description: string;
+
+ if (err.validationErrors?._errors) {
+ // User is not super admin
+ description = err.validationErrors._errors[0];
+ } else {
+ description =
+ err.serverError || "An unknown error occurred";
+ }
+
+ toast.error("Unable to edit event", { description });
toast.dismiss();
toast.error("Failed to delete event");
console.log(err);
@@ -144,26 +156,31 @@ export const columns: ColumnDef[] = [
-
-
- Edit
-
-
-
- Delete
-
+
+ Edit
+
+
+ )}
+ {row.original.isUserAdmin && (
+
+
+ Delete
+
+
+ )}
-
diff --git a/apps/web/src/components/registration/RegisterForm.tsx b/apps/web/src/components/registration/RegisterForm.tsx
index 8c37967f..85abd141 100644
--- a/apps/web/src/components/registration/RegisterForm.tsx
+++ b/apps/web/src/components/registration/RegisterForm.tsx
@@ -81,6 +81,7 @@ import {
encodeFileAsBase64,
decodeBase64AsFile,
} from "@/lib/utils/shared/files";
+import { useDebouncedCallback } from "use-debounce";
export default function RegisterForm({
defaultEmail,
@@ -134,6 +135,7 @@ export default function RegisterForm({
const hackerFormData = localStorage.getItem(
HACKER_REGISTRATION_STORAGE_KEY,
);
+ console.log(hackerFormData);
if (hackerFormData) {
try {
const parsed = JSON.parse(hackerFormData);
@@ -215,14 +217,21 @@ export default function RegisterForm({
}
}, []);
- // might be good to debounce later on
+ const debouncedLocalStorageWrite = useDebouncedCallback(
+ // function
+ () => {
+ localStorage.setItem(
+ HACKER_REGISTRATION_STORAGE_KEY,
+ JSON.stringify({
+ ...form.getValues(),
+ }),
+ );
+ },
+ 1000,
+ );
+
form.watch(() => {
- localStorage.setItem(
- HACKER_REGISTRATION_STORAGE_KEY,
- JSON.stringify({
- ...form.getValues(),
- }),
- );
+ debouncedLocalStorageWrite();
});
// use action logic
@@ -307,8 +316,6 @@ export default function RegisterForm({
},
);
- alert(uploadedFileUrl);
-
resume = uploadedFileUrl;
}
runRegisterUser({ ...data, resume });
diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts
index 4a076d57..2de61d33 100644
--- a/apps/web/src/lib/constants/index.ts
+++ b/apps/web/src/lib/constants/index.ts
@@ -7,7 +7,7 @@ export const UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE = "23505";
export const UNIQUE_KEY_MAPPER_DEFAULT_KEY =
"default" as keyof typeof c.db.uniqueKeyMapper;
export const PAYLOAD_TOO_LARGE_CODE = 413;
-export const HACKER_REGISTRATION_STORAGE_KEY = "hackerRegistrationData";
+export const HACKER_REGISTRATION_STORAGE_KEY = `${c.hackathonName}_${c.itteration}_hackerRegistrationData`;
export const HACKER_REGISTRATION_RESUME_STORAGE_KEY =
"hackerRegistrationResume";
export const NOT_LOCAL_SCHOOL = "NOT_LOCAL_SCHOOL";
diff --git a/apps/web/src/lib/safe-action.ts b/apps/web/src/lib/safe-action.ts
index 8755206f..822408a9 100644
--- a/apps/web/src/lib/safe-action.ts
+++ b/apps/web/src/lib/safe-action.ts
@@ -5,6 +5,7 @@ import {
import { auth } from "@clerk/nextjs/server";
import { getUser } from "db/functions";
import { z } from "zod";
+import { isUserAdmin } from "./utils/server/admin";
export const publicAction = createSafeActionClient();
@@ -21,24 +22,27 @@ export const authenticatedAction = publicAction.use(
},
);
-export const adminAction = authenticatedAction.use(async ({ next, ctx }) => {
- const user = await getUser(ctx.userId);
- if (!user || (user.role !== "admin" && user.role !== "super_admin")) {
- returnValidationErrors(z.null(), {
- _errors: ["Unauthorized (Not Admin)"],
- });
- }
- return next({ ctx: { user, ...ctx } });
-});
-
-export const superAdminAction = authenticatedAction.use(
+export const volunteerAction = authenticatedAction.use(
async ({ next, ctx }) => {
const user = await getUser(ctx.userId);
- if (!user || user.role !== "super_admin") {
+ if (
+ !user ||
+ !["admin", "super_admin", "volunteer"].includes(user.role)
+ ) {
returnValidationErrors(z.null(), {
- _errors: ["Unauthorized (Not Super Admin)"],
+ _errors: ["Unauthorized (Not Admin)"],
});
}
return next({ ctx: { user, ...ctx } });
},
);
+
+export const adminAction = authenticatedAction.use(async ({ next, ctx }) => {
+ const user = await getUser(ctx.userId);
+ if (!user || !isUserAdmin(user)) {
+ returnValidationErrors(z.null(), {
+ _errors: ["Unauthorized (Not Admin)"],
+ });
+ }
+ return next({ ctx: { user, ...ctx } });
+});
diff --git a/apps/web/src/lib/utils/server/admin.ts b/apps/web/src/lib/utils/server/admin.ts
index 440a6c38..d91062cb 100644
--- a/apps/web/src/lib/utils/server/admin.ts
+++ b/apps/web/src/lib/utils/server/admin.ts
@@ -1,5 +1,5 @@
import type { User } from "db/types";
export function isUserAdmin(user: User) {
- return user.role === "admin" || user.role === "super_admin";
+ return ["admin", "super_admin"].includes(user.role);
}
diff --git a/apps/web/src/lib/utils/server/file-upload.ts b/apps/web/src/lib/utils/server/file-upload.ts
index aa18e5d6..ac1c39be 100644
--- a/apps/web/src/lib/utils/server/file-upload.ts
+++ b/apps/web/src/lib/utils/server/file-upload.ts
@@ -7,7 +7,7 @@ export async function del(url: string): Promise {
const key = url.split("=")[1];
const cmd = new DeleteObjectCommand({
- Bucket: staticUploads.bucketName,
+ Bucket: process.env.R2_BUCKET_NAME,
Key: key,
});
diff --git a/apps/web/src/validators/shared/registration.ts b/apps/web/src/validators/shared/registration.ts
index 83f52c9f..fde03f7f 100644
--- a/apps/web/src/validators/shared/registration.ts
+++ b/apps/web/src/validators/shared/registration.ts
@@ -204,9 +204,6 @@ export const hackerRegistrationValidatorLocalStorage =
text: z.string().min(1).max(50),
}),
)
- .min(1, {
- message: "You must have at least one skill",
- })
.max(c.registration.maxNumberOfSkills, {
message: `You cannot have more than ${c.registration.maxNumberOfSkills} skills`,
}),
diff --git a/packages/config/hackkit.config.ts b/packages/config/hackkit.config.ts
index e3a37bd8..ff57e404 100644
--- a/packages/config/hackkit.config.ts
+++ b/packages/config/hackkit.config.ts
@@ -912,11 +912,10 @@ const c = {
Overview: "/admin",
Users: "/admin/users",
Events: "/admin/events",
- Points: "/admin/points",
+ // Points: "/admin/points", -- commented out until implemented
"Hackathon Check-in": "/admin/check-in",
Toggles: "/admin/toggles",
},
- // TODO: Can remove days? Pretty sure they're dynamic now.
},
eventTypes: {
Meal: "#FFC107",
@@ -986,7 +985,6 @@ const c = {
} as const;
const staticUploads = {
- bucketName: "acm-userdata",
bucketHost: "/api/upload/resume/view",
bucketResumeBaseUploadUrl: `${c.hackathonName}/${c.itteration}/resumes`,
} as const;
@@ -1021,8 +1019,8 @@ const publicRoutes = [
/^\/user\//,
"/404",
"/bugreport",
- "/sign-in",
- "/sign-up",
+ /^\/sign-in(\/.*)?$/,
+ /^\/sign-up(\/.*)?$/,
];
export default c;