mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:31:26 -05:00
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:
4
.replit
4
.replit
@@ -33,3 +33,7 @@ outputType = "webview"
|
||||
[[ports]]
|
||||
localPort = 5000
|
||||
externalPort = 80
|
||||
|
||||
[[ports]]
|
||||
localPort = 42081
|
||||
externalPort = 3000
|
||||
|
||||
@@ -47,7 +47,12 @@ function AppContent() {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<LocationAutoDetectProvider />
|
||||
<BrowserRouter>
|
||||
<BrowserRouter
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
}}
|
||||
>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<div className="min-h-screen flex flex-col">
|
||||
|
||||
@@ -544,7 +544,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
{
|
||||
body: {
|
||||
itemIds: failedItems.map(i => i.id),
|
||||
userId: user?.id,
|
||||
submissionId: item.id
|
||||
}
|
||||
}
|
||||
@@ -813,7 +812,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
{
|
||||
body: {
|
||||
itemIds: submissionItems.map(i => i.id),
|
||||
userId: user?.id,
|
||||
submissionId: item.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,6 @@ export function SubmissionReviewManager({
|
||||
const { data, error } = await supabase.functions.invoke('process-selective-approval', {
|
||||
body: {
|
||||
itemIds: Array.from(selectedItemIds),
|
||||
userId: user.id,
|
||||
submissionId
|
||||
}
|
||||
});
|
||||
@@ -330,7 +329,6 @@ export function SubmissionReviewManager({
|
||||
const { data, error } = await supabase.functions.invoke('process-selective-approval', {
|
||||
body: {
|
||||
itemIds: [itemId],
|
||||
userId: user.id,
|
||||
submissionId
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ const corsHeaders = {
|
||||
|
||||
interface ApprovalRequest {
|
||||
itemIds: string[];
|
||||
userId: string;
|
||||
submissionId: string;
|
||||
}
|
||||
|
||||
@@ -49,12 +48,63 @@ serve(async (req) => {
|
||||
}
|
||||
|
||||
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(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
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
|
||||
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
|
||||
if (!submissionId || typeof submissionId !== 'string' || submissionId.trim() === '') {
|
||||
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
|
||||
const { data: items, error: fetchError } = await supabase
|
||||
@@ -241,7 +276,7 @@ serve(async (req) => {
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
status: allApproved ? 'approved' : 'partially_approved',
|
||||
reviewer_id: userId,
|
||||
reviewer_id: authenticatedUserId,
|
||||
reviewed_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', submissionId);
|
||||
|
||||
@@ -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
|
||||
let requestBody;
|
||||
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
|
||||
let requestBody;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user