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 a7c1ca41..3abed82e 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"); @@ -124,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 { @@ -161,18 +171,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() {