Skip to content
Open
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ yarn-error.log*

# local env files
.env*.local
.env.*
.env
bun.lock
.env.*
!.env.example

Expand Down
6 changes: 5 additions & 1 deletion acceptance-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

This directory contains scripts for processing applicant acceptances and sending acceptance emails. The entire process is handled through a single command.


## Prerequisites

1. Ensure you have Node.js installed
Expand Down Expand Up @@ -68,4 +69,7 @@ The script provides real-time console output showing:
- Number of applicants being processed
- Current applicant status
- Success/failure of each operation
- Final summary with success/failure counts
- Final summary with success/failure counts

## Challenges
- Admins can create a challenge
15 changes: 8 additions & 7 deletions app/(dashboard)/applications/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import { isAdmin } from "@/lib/utils";
import Link from "next/link";
import { ExternalLink } from "lucide-react";

const HackerPage = async ({
params,
}: {
params: {
id: string;
};
}) => {
const HackerPage = async (
props: {
params: Promise<{
id: string;
}>;
}
) => {
const params = await props.params;
const { id } = params;
const user = await getCurrentUser();
const currentUser = user;
Expand Down
5 changes: 3 additions & 2 deletions app/(dashboard)/applications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import { getCurrentUser } from "@/auth";
import { ApplicationSearch } from "@/components/search/ApplicationSearch";

interface ApplicationsPageProps {
searchParams: { [key: string]: string | string[] | undefined };
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

const ApplicationsPage = async ({ searchParams }: ApplicationsPageProps) => {
const ApplicationsPage = async (props: ApplicationsPageProps) => {
const searchParams = await props.searchParams;
const user = await getCurrentUser();

if (!user || user.role !== "admin") {
Expand Down
67 changes: 67 additions & 0 deletions app/(dashboard)/challenges/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/auth";
import { count, eq } from "drizzle-orm";

import { db } from "@/lib/db";
import { challenges, challengesSubmitted } from "@/lib/db/schema";
import { AdminChallengeDataTable } from "@/components/challenges/AdminChallengeDataTable";

export default async function AdminPage() {
const currentUser = await getCurrentUser();

if (!currentUser) {
redirect("/sign-in");
}

if (currentUser.role !== "admin") {
redirect("/");
}

const challengesData = await db
.select({
id: challenges.id,
name: challenges.name,
category: challenges.category,
points: challenges.points,
difficulty: challenges.difficulty,

shortDescription: challenges.shortDescription,
instructions: challenges.instructions,
hints: challenges.hints,
qrCode: challenges.qrCode,
submissionInstructions: challenges.submissionInstructions,
maxCompletions: challenges.maxCompletions,
enabled: challenges.enabled,
currentCompletions: count(challengesSubmitted.id),
})
.from(challenges)
.leftJoin(
challengesSubmitted,
eq(challenges.id, challengesSubmitted.challengeId),
)
.groupBy(challenges.id);

// Drizzle count returns number, but sometimes it might be returned as string or parsed differently depending on driver.
// Neon usually returns number. But let's map to make sure type matches.
const formattedData = challengesData.map((c) => ({
...c,
currentCompletions: Number(c.currentCompletions),
}));

return (
<main className="container space-y-8 py-8">
<section className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">
Admin Dashboard
</h1>
<p className="text-sm text-muted-foreground">
Manage challenges and view stats.
</p>
</section>

<section className="space-y-4">
<AdminChallengeDataTable data={formattedData} />
</section>
</main>
);
}
5 changes: 3 additions & 2 deletions app/(dashboard)/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { LogsStats } from "@/components/logs/LogsStats";
import { LogList } from "@/components/logs/LogList";

interface LogsPageProps {
searchParams: { [key: string]: string | string[] | undefined };
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

const LogsPage = async ({ searchParams }: LogsPageProps) => {
const LogsPage = async (props: LogsPageProps) => {
const searchParams = await props.searchParams;
const { logs, totalLogs, start, params } = await getLogs({
page: searchParams["page"] as string,
perPage: searchParams["perPage"] as string,
Expand Down
7 changes: 3 additions & 4 deletions app/(dashboard)/review-applications/decisions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ import { RESULTS_PER_PAGE } from "@/lib/constants";
import { count } from "drizzle-orm";

interface AdminApplicationsPageProps {
searchParams: { [key: string]: string | string[] | undefined };
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function AdminApplicationsPage({
searchParams,
}: AdminApplicationsPageProps) {
export default async function AdminApplicationsPage(props: AdminApplicationsPageProps) {
const searchParams = await props.searchParams;
const user = await getCurrentUser();

if (!user?.id || !isAdmin(user.role)) {
Expand Down
11 changes: 6 additions & 5 deletions app/(dashboard)/reviewers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import PageBanner from "@/components/PageBanner";
import ReviewerStats from "@/components/review/ReviewerStats";
import { getReviewers, getReviewerStats } from "@/lib/services/reviewer-stats";

export default async function ReviewerStatsPage({
searchParams,
}: {
searchParams: { reviewer?: string };
}) {
export default async function ReviewerStatsPage(
props: {
searchParams: Promise<{ reviewer?: string }>;
}
) {
const searchParams = await props.searchParams;
const user = await getCurrentUser();

if (!user?.id || !isAdmin(user.role)) {
Expand Down
15 changes: 8 additions & 7 deletions app/(dashboard)/users/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { getUserById } from "@/data/user";
import { redirect } from "next/navigation";
import React from "react";

const UserPage = async ({
params,
}: {
params: {
id: string;
};
}) => {
const UserPage = async (
props: {
params: Promise<{
id: string;
}>;
}
) => {
const params = await props.params;
const { id } = params;
const currentUser = await getCurrentUser();

Expand Down
5 changes: 3 additions & 2 deletions app/(dashboard)/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { UsersSearchControls } from "@/components/UsersSearchControls";
import { getUsers } from "@/data/users-page";

interface UsersPageProps {
searchParams: { [key: string]: string | string[] | undefined };
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

const UsersPage = async ({ searchParams }: UsersPageProps) => {
const UsersPage = async (props: UsersPageProps) => {
const searchParams = await props.searchParams;
const user = await getCurrentUser();

if (!user || user.role !== "admin") {
Expand Down
95 changes: 95 additions & 0 deletions app/api/challenges/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/auth";
import { eq } from "drizzle-orm";

import { db } from "@/lib/db";
import { challenges } from "@/lib/db/schema";
import { challengeSchema } from "@/lib/validations/challenge";

// We'll use a partial schema for updates so we don't need to send the whole object
const updateChallengeSchema = challengeSchema.partial();

export async function PATCH(
req: NextRequest,
props: { params: Promise<{ id: string }> },
) {
try {
const params = await props.params;
const currentUser = await getCurrentUser();

if (!currentUser || currentUser.role !== "admin") {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 },
);
}

const body = await req.json();
const validationResult = updateChallengeSchema.safeParse(body);

if (!validationResult.success) {
return NextResponse.json(
{
success: false,
message: "Invalid request data",
error: validationResult.error.errors,
},
{ status: 400 },
);
}

const { data } = validationResult;

await db.update(challenges).set(data).where(eq(challenges.id, params.id));

return NextResponse.json({
success: true,
message: "Challenge updated successfully",
});
} catch (error) {
console.error("Error updating challenge:", error);
return NextResponse.json(
{
success: false,
message: "Internal server error",
},
{ status: 500 },
);
}
}

export async function DELETE(
_req: NextRequest,
props: { params: Promise<{ id: string }> },
) {
try {
const params = await props.params;
const currentUser = await getCurrentUser();

if (
!currentUser ||
!(currentUser.role === "admin" || currentUser.role == "organizer")
) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 },
);
}

await db.delete(challenges).where(eq(challenges.id, params.id));

return NextResponse.json({
success: true,
message: "Challenge deleted successfully",
});
} catch (error) {
console.error("Error deleting challenge:", error);
return NextResponse.json(
{
success: false,
message: "Internal server error",
},
{ status: 500 },
);
}
}
56 changes: 56 additions & 0 deletions app/api/challenges/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/auth";

import { db } from "@/lib/db";
import { challenges } from "@/lib/db/schema";
import { challengeSchema } from "@/lib/validations/challenge";

export async function POST(req: NextRequest) {
try {
const currentUser = await getCurrentUser();

if (!currentUser || currentUser.role !== "admin") {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 },
);
}

const body = await req.json();

const validationResult = challengeSchema.safeParse(body);

if (!validationResult.success) {
return NextResponse.json(
{
success: false,
message: "Invalid request data",
error: validationResult.error.errors,
},
{ status: 400 },
);
}

const { data } = validationResult;

await db.insert(challenges).values({
...data,
maxCompletions: data.maxCompletions ?? null,
hints: data.hints || [],
});

return NextResponse.json({
success: true,
message: "Challenge created successfully",
});
} catch (error) {
console.error("Error creating challenge:", error);
return NextResponse.json(
{
success: false,
message: "Internal server error",
},
{ status: 500 },
);
}
}
Loading