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 {