Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
545 changes: 545 additions & 0 deletions app/admin/forms/core-team/page.tsx

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
HandHeart,
ClipboardCheck,
Award,
Crown,
} from "lucide-react"
import { useAuth } from "@/lib/hooks/useAuth"

Expand Down Expand Up @@ -119,6 +120,11 @@ const sidebarItems: SidebarGroupType[] = [
url: "/admin/forms/volunteer",
icon: HandHeart,
},
{
title: "Core Team",
url: "/admin/forms/core-team",
icon: Crown,
},
],
},
{
Expand Down
97 changes: 97 additions & 0 deletions app/api/admin-core-team/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

function getSupabaseClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
}
Comment on lines +1 to +9
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Blocker: Admin API lacks authentication/authorization and uses service role key.

Endpoints are publicly accessible and run with SUPABASE_SERVICE_ROLE_KEY, bypassing RLS. This exposes PII and allows arbitrary updates. Require an authenticated admin before any DB access.

Apply this diff to gate all handlers by session and admin check (using Supabase server client) and avoid exposing the service role to unauthenticated callers:

 import { NextResponse } from 'next/server';
-import { createClient } from '@supabase/supabase-js';
+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!
+  );
+}
+
+function getServerClient() {
+  return createServerClient(
+    process.env.NEXT_PUBLIC_SUPABASE_URL!,
+    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+    { cookies }
+  );
+}
+
+async function requireAdmin() {
+  const supa = 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 };
+}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
function getSupabaseClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
}
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
// Server-side clients
function getServiceClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
}
function getServerClient() {
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies }
);
}
async function requireAdmin() {
const supa = 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 };
}
πŸ€– Prompt for AI Agents
In app/api/admin-core-team/route.ts lines 1-9, the current code creates a
Supabase client with the SERVICE_ROLE key and exposes admin endpoints without
authentication; update handlers to require an authenticated admin session before
any DB access: (1) stop using the SERVICE_ROLE key to build the public-facing
client β€” create a request-scoped Supabase client using NEXT_PUBLIC_SUPABASE_URL
and NEXT_PUBLIC_SUPABASE_ANON_KEY (or the server auth helper) so you can read
the session from request cookies/headers; (2) in every handler, call the server
auth to get the session/user and query your users/profiles table (or check a
custom claim) to verify the user is an admin; if no session or not admin, return
401/403 immediately; (3) only after admin verification, if you must perform
privileged actions that require the SERVICE_ROLE key, construct a separate
server-only Supabase client using SUPABASE_SERVICE_ROLE_KEY inside the protected
branch so the service role is never exposed to unauthenticated callers. Ensure
all DB reads/updates in these routes are gated by that admin check.


export async function GET() {
try {
const supabase = getSupabaseClient();

const { data, error } = await supabase
.from('core_team_applications')
.select('*')
.order('created_at', { ascending: false });

if (error) {
console.error('Error fetching core team applications:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}

return NextResponse.json({ applications: data });
} catch (error) {
Comment on lines +11 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Enforce admin check and limit selected columns; add pagination.

Return only needed columns, and avoid unbounded scans.

 export async function GET() {
   try {
-        const supabase = getSupabaseClient();
+        const auth = await requireAdmin();
+        if (!auth.ok) return auth.resp;
+        const supabase = getServiceClient();
+        // Optional: parse ?limit=&offset= for pagination
+        const url = new URL(globalThis.location?.href ?? 'http://localhost');
+        const limit = Number(url.searchParams.get('limit') ?? 100);
+        const offset = Number(url.searchParams.get('offset') ?? 0);
         
-        const { data, error } = await supabase
+        const { data, error } = await supabase
             .from('core_team_applications')
-            .select('*')
-            .order('created_at', { ascending: false });
+            .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 })
+            .range(offset, offset + limit - 1);

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In app/api/admin-core-team/route.ts around lines 11 to 26, enforce an admin
authorization check before querying, limit the selected columns to only the
fields needed (e.g., id, user_id, status, created_at β€” replace with your exact
required list) instead of select('*'), and add pagination (accept page/limit or
range params and apply .range() or .limit()/.offset() on the Supabase query) to
avoid unbounded scans; return 403 if the caller is not an admin and include
pagination metadata in the response.

console.error('Unexpected error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

export async function POST(req: Request) {
try {
const body = await req.json();
const { id, status, notes } = body;

if (!id || !status) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}

const supabase = getSupabaseClient();

const { data, error } = await supabase
.from('core_team_applications')
.update({
status,
updated_at: new Date().toISOString(),
...(notes && { notes })
})
.eq('id', id)
.select()
.single();

if (error) {
console.error('Error updating core team application:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}

return NextResponse.json({ application: data });
} catch (error) {
console.error('Unexpected error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
Comment on lines +32 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

POST handler is updating records; either remove it or make semantics clear and validate status.

POST currently duplicates PATCH behavior and permits arbitrary status values. Either drop POST or constrain to β€œstatus change” with strict validation.

 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) {
+        const ALLOWED = new Set(['pending','approved','rejected']);
+        if (!id || !status || !ALLOWED.has(status)) {
             return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
         }
 
-        const supabase = getSupabaseClient();
+        const supabase = getServiceClient();

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In app/api/admin-core-team/route.ts around lines 32 to 64, the POST handler
currently behaves like a PATCH (it updates records) and allows arbitrary status
values; either remove the POST or make its intent explicit by restricting it to
status changes: change semantics to only accept a defined set of statuses
(validate status against an allowlist), require id and validated status, only
update status, updated_at and optional notes, and return 400 for invalid status;
additionally consider moving this logic to a PATCH endpoint or rename the
handler to reflect an update operation to avoid duplicating PATCH behavior.


export async function PATCH(req: Request) {
try {
const body = await req.json();
const { id, ...updates } = body;

if (!id) {
return NextResponse.json({ error: 'Missing application ID' }, { status: 400 });
}

const supabase = getSupabaseClient();

const { data, error } = await supabase
.from('core_team_applications')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select()
.single();

if (error) {
console.error('Error updating core team application:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}

return NextResponse.json({ application: data });
} catch (error) {
console.error('Unexpected error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
Comment on lines +66 to +97
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

PATCH allows arbitrary field updates including PII/user_id; restrict to an allowlist.

Limit updates to { status, notes } and validate values. Always gate by admin.

 export async function PATCH(req: Request) {
   try {
-        const body = await req.json();
-        const { id, ...updates } = body;
+        const auth = await requireAdmin();
+        if (!auth.ok) return auth.resp;
+        const body = await req.json() as any;
+        const { id, status, notes } = body;
 
-        if (!id) {
+        const ALLOWED = new Set(['pending','approved','rejected']);
+        if (!id || (status && !ALLOWED.has(status))) {
             return NextResponse.json({ error: 'Missing application ID' }, { status: 400 });
         }
 
-        const supabase = getSupabaseClient();
+        const supabase = getServiceClient();
         
-        const { data, error } = await supabase
+        const payload: Record<string, any> = { updated_at: new Date().toISOString() };
+        if (typeof status === 'string') payload.status = status;
+        if (typeof notes === 'string') payload.notes = notes;
+
+        const { data, error } = await supabase
             .from('core_team_applications')
-            .update({ 
-                ...updates,
-                updated_at: new Date().toISOString()
-            })
+            .update(payload)
             .eq('id', id)
             .select()
             .single();
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function PATCH(req: Request) {
try {
const body = await req.json();
const { id, ...updates } = body;
if (!id) {
return NextResponse.json({ error: 'Missing application ID' }, { status: 400 });
}
const supabase = getSupabaseClient();
const { data, error } = await supabase
.from('core_team_applications')
.update({
...updates,
updated_at: new Date().toISOString()
})
.eq('id', id)
.select()
.single();
if (error) {
console.error('Error updating core team application:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ application: data });
} catch (error) {
console.error('Unexpected error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function PATCH(req: Request) {
try {
const auth = await requireAdmin();
if (!auth.ok) return auth.resp;
const body = await req.json() as any;
const { id, status, notes } = body;
const ALLOWED = new Set(['pending','approved','rejected']);
if (!id || (status && !ALLOWED.has(status))) {
return NextResponse.json({ error: 'Missing application ID' }, { status: 400 });
}
const supabase = getServiceClient();
const payload: Record<string, any> = { updated_at: new Date().toISOString() };
if (typeof status === 'string') payload.status = status;
if (typeof notes === 'string') payload.notes = notes;
const { data, error } = await supabase
.from('core_team_applications')
.update(payload)
.eq('id', id)
.select()
.single();
if (error) {
console.error('Error updating core team application:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ application: data });
} catch (error) {
console.error('Unexpected error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
πŸ€– Prompt for AI Agents
In app/api/admin-core-team/route.ts around lines 66 to 97, the PATCH handler
currently accepts arbitrary fields from the request body and can update PII or
user_id; restrict updates to an explicit allowlist and validate them. Extract
only allowed fields (status and notes) from the parsed body, validate status
against the permitted enum/strings and sanitize/limit notes length, reject the
request with 400 if validation fails, and ensure the operation is only performed
after verifying the requester is an admin (e.g., check session/authorization
before calling Supabase). Use the filtered updates object when calling
.update(), set updated_at as before, and return a 403 if the user is not an
admin.

Loading
Loading