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;