mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -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]]
|
[[ports]]
|
||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 42081
|
||||||
|
externalPort = 3000
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user