From f85d070224948f3d66bf7649056b50c69b15328c Mon Sep 17 00:00:00 2001
From: Deepak Pandey
Date: Sun, 14 Sep 2025 21:27:12 +0530
Subject: [PATCH 1/2] fix: Address CodeRabbit security warnings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
🔒 Security Fixes:
- Add admin authentication to admin-core-team API endpoints
- Prevent unauthorized access to admin data
- Add proper user session validation
- Use service role only for admin operations
🛡️ CSV Injection Protection:
- Sanitize CSV export data to prevent formula injection
- Add prefix for cells starting with =, +, -, @
🔗 Navigation Improvements:
- Replace window.location.href with Next.js Link components
- Improve SPA navigation and accessibility
- Fix all form authentication redirects
✅ Admin UI Security:
- Add client-side admin access guards
- Prevent non-admin users from accessing admin pages
- Show proper forbidden messages
All critical security vulnerabilities addressed per CodeRabbit review.
---
app/admin/forms/core-team/page.tsx | 28 ++++++----
app/api/admin-core-team/route.ts | 72 ++++++++++++++++++++-----
components/forms/collaboration-form.tsx | 5 +-
components/forms/core-team-form.tsx | 5 +-
components/forms/judges-form.tsx | 5 +-
components/forms/mentor-form.tsx | 5 +-
components/forms/sponsorship-form.tsx | 5 +-
components/forms/volunteer-form.tsx | 5 +-
8 files changed, 97 insertions(+), 33 deletions(-)
diff --git a/app/admin/forms/core-team/page.tsx b/app/admin/forms/core-team/page.tsx
index a7c1ca41..1c000477 100644
--- a/app/admin/forms/core-team/page.tsx
+++ b/app/admin/forms/core-team/page.tsx
@@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { apiFetch } from "@/lib/api-fetch";
+import { useAuth } from "@/lib/hooks/useAuth";
import {
Dialog,
DialogContent,
@@ -61,6 +62,7 @@ interface CoreTeamApplication {
}
export default function AdminCoreTeamPage() {
+ const { loading: authLoading, is_admin } = useAuth();
const [applications, setApplications] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
@@ -70,6 +72,13 @@ export default function AdminCoreTeamPage() {
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
const [savingStatus, setSavingStatus] = useState>({});
+ if (authLoading) {
+ return Loading...
;
+ }
+ if (!is_admin) {
+ return Forbidden: admin access required.
;
+ }
+
// Fetch applications
useEffect(() => {
const fetchApplications = async () => {
@@ -161,18 +170,19 @@ export default function AdminCoreTeamPage() {
// Export to CSV
const handleExportCSV = () => {
+ const sanitize = (v: string) => (/^[-+=@]/.test(v) ? `'${v}` : v);
const csvContent = [
["Name", "Email", "Phone", "Location", "Occupation", "Company", "Preferred Role", "Status", "Applied Date"],
...filteredApplications.map(app => [
- `${app.first_name} ${app.last_name}`,
- app.email,
- app.phone || "",
- app.location,
- app.occupation,
- app.company || "",
- app.preferred_role,
- app.status,
- new Date(app.created_at).toLocaleDateString()
+ sanitize(`${app.first_name} ${app.last_name}`),
+ sanitize(app.email),
+ sanitize(app.phone || ""),
+ sanitize(app.location),
+ sanitize(app.occupation),
+ sanitize(app.company || ""),
+ sanitize(app.preferred_role),
+ sanitize(app.status),
+ sanitize(new Date(app.created_at).toLocaleDateString())
])
].map(row => row.map(cell => `"${cell}"`).join(",")).join("\n");
diff --git a/app/api/admin-core-team/route.ts b/app/api/admin-core-team/route.ts
index f4f1ff5d..4039944d 100644
--- a/app/api/admin-core-team/route.ts
+++ b/app/api/admin-core-team/route.ts
@@ -1,20 +1,61 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
+import { createServerClient } from '@supabase/ssr';
+import { cookies } from 'next/headers';
-function getSupabaseClient() {
- return createClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.SUPABASE_SERVICE_ROLE_KEY!
- );
+// Server-side clients
+function getServiceClient() {
+ return createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
+ );
+}
+
+async function getServerClient() {
+ const cookieStore = await cookies();
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value, options }) => {
+ cookieStore.set(name, value, options);
+ });
+ },
+ },
+ }
+ );
+}
+
+async function requireAdmin() {
+ const supa = await getServerClient();
+ const { data: { user }, error } = await supa.auth.getUser();
+ if (error || !user) {
+ return { ok: false, resp: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) };
+ }
+ // Check admin flag from profiles (service client to bypass RLS for lookup only)
+ const svc = getServiceClient();
+ const { data: profile, error: pErr } = await svc.from('profiles').select('is_admin').eq('id', user.id).single();
+ if (pErr || !profile?.is_admin) {
+ return { ok: false, resp: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) };
+ }
+ return { ok: true };
}
export async function GET() {
try {
- const supabase = getSupabaseClient();
+ const auth = await requireAdmin();
+ if (!auth.ok) return auth.resp;
+
+ const supabase = getServiceClient();
const { data, error } = await supabase
.from('core_team_applications')
- .select('*')
+ .select('id,first_name,last_name,email,phone,location,occupation,company,experience,skills,portfolio,preferred_role,availability,commitment,motivation,vision,previous_experience,social_media,references_info,additional_info,status,user_id,created_at,updated_at')
.order('created_at', { ascending: false });
if (error) {
@@ -31,14 +72,18 @@ export async function GET() {
export async function POST(req: Request) {
try {
+ const auth = await requireAdmin();
+ if (!auth.ok) return auth.resp;
+
const body = await req.json();
- const { id, status, notes } = body;
+ const { id, status, notes } = body as { id?: number; status?: string; notes?: string };
- if (!id || !status) {
- return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
+ const ALLOWED_STATUSES = new Set(['pending','approved','rejected']);
+ if (!id || !status || !ALLOWED_STATUSES.has(status)) {
+ return NextResponse.json({ error: 'Missing required fields or invalid status' }, { status: 400 });
}
- const supabase = getSupabaseClient();
+ const supabase = getServiceClient();
const { data, error } = await supabase
.from('core_team_applications')
@@ -65,6 +110,9 @@ export async function POST(req: Request) {
export async function PATCH(req: Request) {
try {
+ const auth = await requireAdmin();
+ if (!auth.ok) return auth.resp;
+
const body = await req.json();
const { id, ...updates } = body;
@@ -72,7 +120,7 @@ export async function PATCH(req: Request) {
return NextResponse.json({ error: 'Missing application ID' }, { status: 400 });
}
- const supabase = getSupabaseClient();
+ const supabase = getServiceClient();
const { data, error } = await supabase
.from('core_team_applications')
diff --git a/components/forms/collaboration-form.tsx b/components/forms/collaboration-form.tsx
index 26b34750..51b21e11 100644
--- a/components/forms/collaboration-form.tsx
+++ b/components/forms/collaboration-form.tsx
@@ -11,6 +11,7 @@ import { Building2, FileText, Loader2 } from "lucide-react";
import { createBrowserClient } from "@supabase/ssr";
import { toast } from "sonner";
import { useAuth } from "@/lib/hooks/useAuth";
+import Link from "next/link";
export function CollaborationForm() {
const { user, loading: authLoading } = useAuth();
@@ -143,9 +144,9 @@ export function CollaborationForm() {
diff --git a/components/forms/core-team-form.tsx b/components/forms/core-team-form.tsx
index c5107c8f..ea5de80b 100644
--- a/components/forms/core-team-form.tsx
+++ b/components/forms/core-team-form.tsx
@@ -12,6 +12,7 @@ import { Crown, Send, Users, Code2, Camera, PenTool, Target, Zap } from "lucide-
import { createBrowserClient } from "@supabase/ssr";
import { toast } from "sonner";
import { useAuth } from "@/lib/hooks/useAuth";
+import Link from "next/link";
export function CoreTeamForm() {
const { user, loading: authLoading } = useAuth();
@@ -200,9 +201,9 @@ export function CoreTeamForm() {
diff --git a/components/forms/judges-form.tsx b/components/forms/judges-form.tsx
index 5cdda0c1..694256ef 100644
--- a/components/forms/judges-form.tsx
+++ b/components/forms/judges-form.tsx
@@ -12,6 +12,7 @@ import { Award, Send, Code2, Users, Globe } from "lucide-react";
import { createBrowserClient } from "@supabase/ssr";
import { toast } from "sonner";
import { useAuth } from "@/lib/hooks/useAuth";
+import Link from "next/link";
export function JudgesForm() {
const { user, loading: authLoading } = useAuth();
@@ -184,9 +185,9 @@ export function JudgesForm() {
);
diff --git a/components/forms/mentor-form.tsx b/components/forms/mentor-form.tsx
index 2815b605..a4dad33c 100644
--- a/components/forms/mentor-form.tsx
+++ b/components/forms/mentor-form.tsx
@@ -12,6 +12,7 @@ import { Lightbulb, Send, Code2, Users, GraduationCap, MessageSquare } from "luc
import { createBrowserClient } from "@supabase/ssr";
import { toast } from "sonner";
import { useAuth } from "@/lib/hooks/useAuth";
+import Link from "next/link";
export function MentorForm() {
const { user, loading: authLoading } = useAuth();
@@ -229,9 +230,9 @@ export function MentorForm() {
diff --git a/components/forms/sponsorship-form.tsx b/components/forms/sponsorship-form.tsx
index 8b89a0cf..d20e1c83 100644
--- a/components/forms/sponsorship-form.tsx
+++ b/components/forms/sponsorship-form.tsx
@@ -12,6 +12,7 @@ import { Trophy, Send, Building2 } from "lucide-react";
import { createBrowserClient } from "@supabase/ssr";
import { toast } from "sonner";
import { useAuth } from "@/lib/hooks/useAuth";
+import Link from "next/link";
export function SponsorshipForm() {
const { user, loading: authLoading } = useAuth();
@@ -188,9 +189,9 @@ export function SponsorshipForm() {
);
diff --git a/components/forms/volunteer-form.tsx b/components/forms/volunteer-form.tsx
index 4b778402..f674b749 100644
--- a/components/forms/volunteer-form.tsx
+++ b/components/forms/volunteer-form.tsx
@@ -12,6 +12,7 @@ import { HandHeart, Send, Calendar, MapPin, Users, Code2, Heart } from "lucide-r
import { createBrowserClient } from "@supabase/ssr";
import { toast } from "sonner";
import { useAuth } from "@/lib/hooks/useAuth";
+import Link from "next/link";
export function VolunteerForm() {
const { user, loading: authLoading } = useAuth();
@@ -176,9 +177,9 @@ export function VolunteerForm() {
From e72d3f295aac5ced3279eba502b65f9fb545cc88 Mon Sep 17 00:00:00 2001
From: Deepak Pandey
Date: Sun, 14 Sep 2025 21:29:16 +0530
Subject: [PATCH 2/2] fix: Resolve React hooks and linting issues
- Fix React hooks rules violation in admin page
- Move admin access check after all hooks
- Remove unused error variable in username page
- All builds now pass successfully
---
app/[username]/page.tsx | 2 +-
app/admin/forms/core-team/page.tsx | 15 ++++++++-------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx
index ea07d2a2..0677bc20 100644
--- a/app/[username]/page.tsx
+++ b/app/[username]/page.tsx
@@ -41,7 +41,7 @@ export default async function UsernamePage({ params }: UsernamePageProps) {
if (error || !profile) {
notFound();
}
- } catch (error) {
+ } catch {
// If there's any error, show 404
notFound();
}
diff --git a/app/admin/forms/core-team/page.tsx b/app/admin/forms/core-team/page.tsx
index 1c000477..3abed82e 100644
--- a/app/admin/forms/core-team/page.tsx
+++ b/app/admin/forms/core-team/page.tsx
@@ -72,13 +72,6 @@ export default function AdminCoreTeamPage() {
const [isViewDialogOpen, setIsViewDialogOpen] = useState(false);
const [savingStatus, setSavingStatus] = useState>({});
- if (authLoading) {
- return Loading...
;
- }
- if (!is_admin) {
- return Forbidden: admin access required.
;
- }
-
// Fetch applications
useEffect(() => {
const fetchApplications = async () => {
@@ -133,6 +126,14 @@ export default function AdminCoreTeamPage() {
return Array.from(new Set(roles)).sort();
}, [applications]);
+ // Admin access check (after all hooks)
+ if (authLoading) {
+ return Loading...
;
+ }
+ if (!is_admin) {
+ return Forbidden: admin access required.
;
+ }
+
// Handle status update
const handleStatusUpdate = async (id: number, newStatus: string) => {
try {