Improve security by verifying user authentication and authorization

Update the 'process-selective-approval' Supabase function to enforce authentication and authorization checks before processing requests. Also, modify the 'upload-image' function to prevent banned users from uploading images. Additionally, enable future React Router v7 features for enhanced navigation.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 6d6e48da-5b1b-47f9-a65c-9fa4a352936a
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7cdf4e95-3f41-4180-b8e3-8ef56d032c0e/6d6e48da-5b1b-47f9-a65c-9fa4a352936a/u05utRo
This commit is contained in:
pac7
2025-10-07 20:12:39 +00:00
parent ff4a1521bb
commit b8787ee6de
6 changed files with 120 additions and 24 deletions

View File

@@ -33,3 +33,7 @@ outputType = "webview"
[[ports]] [[ports]]
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 42081
externalPort = 3000

View File

@@ -47,7 +47,12 @@ function AppContent() {
return ( return (
<TooltipProvider> <TooltipProvider>
<LocationAutoDetectProvider /> <LocationAutoDetectProvider />
<BrowserRouter> <BrowserRouter
future={{
v7_startTransition: true,
v7_relativeSplatPath: true,
}}
>
<Toaster /> <Toaster />
<Sonner /> <Sonner />
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">

View File

@@ -544,7 +544,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
{ {
body: { body: {
itemIds: failedItems.map(i => i.id), itemIds: failedItems.map(i => i.id),
userId: user?.id,
submissionId: item.id submissionId: item.id
} }
} }
@@ -813,7 +812,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
{ {
body: { body: {
itemIds: submissionItems.map(i => i.id), itemIds: submissionItems.map(i => i.id),
userId: user?.id,
submissionId: item.id submissionId: item.id
} }
} }

View File

@@ -156,7 +156,6 @@ export function SubmissionReviewManager({
const { data, error } = await supabase.functions.invoke('process-selective-approval', { const { data, error } = await supabase.functions.invoke('process-selective-approval', {
body: { body: {
itemIds: Array.from(selectedItemIds), itemIds: Array.from(selectedItemIds),
userId: user.id,
submissionId submissionId
} }
}); });
@@ -330,7 +329,6 @@ export function SubmissionReviewManager({
const { data, error } = await supabase.functions.invoke('process-selective-approval', { const { data, error } = await supabase.functions.invoke('process-selective-approval', {
body: { body: {
itemIds: [itemId], itemIds: [itemId],
userId: user.id,
submissionId submissionId
} }
}); });

View File

@@ -8,7 +8,6 @@ const corsHeaders = {
interface ApprovalRequest { interface ApprovalRequest {
itemIds: string[]; itemIds: string[];
userId: string;
submissionId: string; submissionId: string;
} }
@@ -49,12 +48,63 @@ serve(async (req) => {
} }
try { try {
// Verify authentication first with a client that respects RLS
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Authentication required. Please log in.' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Create Supabase client with user's auth token to verify authentication
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY') ?? '';
const supabaseAuth = createClient(supabaseUrl, supabaseAnonKey, {
global: { headers: { Authorization: authHeader } }
});
// Verify JWT and get authenticated user
const { data: { user }, error: authError } = await supabaseAuth.auth.getUser();
if (authError || !user) {
console.error('Auth verification failed:', authError);
return new Response(
JSON.stringify({ error: 'Invalid authentication token.' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
const authenticatedUserId = user.id;
// Create service role client for privileged operations (including role check)
const supabase = createClient( const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
); );
const { itemIds, userId, submissionId }: ApprovalRequest = await req.json(); // Check if user has moderator permissions using service role to bypass RLS
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('role')
.eq('user_id', authenticatedUserId)
.single();
if (profileError || !profile) {
console.error('Failed to fetch profile:', profileError);
return new Response(
JSON.stringify({ error: 'User profile not found.' }),
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
if (profile.role !== 'moderator' && profile.role !== 'admin') {
return new Response(
JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }),
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
const { itemIds, submissionId }: ApprovalRequest = await req.json();
// UUID validation regex // UUID validation regex
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -74,21 +124,6 @@ serve(async (req) => {
); );
} }
// Validate userId
if (!userId || typeof userId !== 'string' || userId.trim() === '') {
return new Response(
JSON.stringify({ error: 'userId is required and must be a non-empty string' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
if (!uuidRegex.test(userId)) {
return new Response(
JSON.stringify({ error: 'userId must be a valid UUID format' }),
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Validate submissionId // Validate submissionId
if (!submissionId || typeof submissionId !== 'string' || submissionId.trim() === '') { if (!submissionId || typeof submissionId !== 'string' || submissionId.trim() === '') {
return new Response( return new Response(
@@ -104,7 +139,7 @@ serve(async (req) => {
); );
} }
console.log('Processing selective approval:', { itemIds, userId, submissionId }); console.log('Processing selective approval:', { itemIds, userId: authenticatedUserId, submissionId });
// Fetch all items for the submission // Fetch all items for the submission
const { data: items, error: fetchError } = await supabase const { data: items, error: fetchError } = await supabase
@@ -241,7 +276,7 @@ serve(async (req) => {
.from('content_submissions') .from('content_submissions')
.update({ .update({
status: allApproved ? 'approved' : 'partially_approved', status: allApproved ? 'approved' : 'partially_approved',
reviewer_id: userId, reviewer_id: authenticatedUserId,
reviewed_at: new Date().toISOString() reviewed_at: new Date().toISOString()
}) })
.eq('id', submissionId); .eq('id', submissionId);

View File

@@ -57,6 +57,34 @@ serve(async (req) => {
) )
} }
// Check if user is banned
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', user.id)
.single()
if (profileError || !profile) {
console.error('Failed to fetch user profile:', profileError)
return new Response(
JSON.stringify({ error: 'User profile not found' }),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
if (profile.banned) {
return new Response(
JSON.stringify({ error: 'Account suspended. Contact support for assistance.' }),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Delete image from Cloudflare // Delete image from Cloudflare
let requestBody; let requestBody;
try { try {
@@ -149,6 +177,34 @@ serve(async (req) => {
) )
} }
// Check if user is banned
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', user.id)
.single()
if (profileError || !profile) {
console.error('Failed to fetch user profile:', profileError)
return new Response(
JSON.stringify({ error: 'User profile not found' }),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
if (profile.banned) {
return new Response(
JSON.stringify({ error: 'Account suspended. Contact support for assistance.' }),
{
status: 403,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Request a direct upload URL from Cloudflare // Request a direct upload URL from Cloudflare
let requestBody; let requestBody;
try { try {