mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:31:12 -05:00
Fix critical Phase 1 issues
This commit is contained in:
@@ -23,9 +23,50 @@ import { useAuth } from '@/hooks/useAuth';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { smartMergeArray } from '@/lib/smartStateUpdate';
|
||||
|
||||
// Type-safe reported content interfaces
|
||||
interface ReportedReview {
|
||||
id: string;
|
||||
title: string | null;
|
||||
content: string | null;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface ReportedProfile {
|
||||
user_id: string;
|
||||
username: string;
|
||||
display_name: string | null;
|
||||
}
|
||||
|
||||
interface ReportedSubmission {
|
||||
id: string;
|
||||
submission_type: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// Union type for all possible reported content
|
||||
type ReportedContent = ReportedReview | ReportedProfile | ReportedSubmission | null;
|
||||
|
||||
// Discriminated union for entity types
|
||||
type ReportEntityType = 'review' | 'profile' | 'content_submission';
|
||||
|
||||
/**
|
||||
* Type guards for reported content
|
||||
*/
|
||||
function isReportedReview(content: ReportedContent): content is ReportedReview {
|
||||
return content !== null && 'rating' in content;
|
||||
}
|
||||
|
||||
function isReportedProfile(content: ReportedContent): content is ReportedProfile {
|
||||
return content !== null && 'username' in content;
|
||||
}
|
||||
|
||||
function isReportedSubmission(content: ReportedContent): content is ReportedSubmission {
|
||||
return content !== null && 'submission_type' in content;
|
||||
}
|
||||
|
||||
interface Report {
|
||||
id: string;
|
||||
reported_entity_type: string;
|
||||
reported_entity_type: ReportEntityType;
|
||||
reported_entity_id: string;
|
||||
report_type: string;
|
||||
reason: string;
|
||||
@@ -35,7 +76,7 @@ interface Report {
|
||||
username: string;
|
||||
display_name?: string;
|
||||
};
|
||||
reported_content?: any;
|
||||
reported_content?: ReportedContent;
|
||||
}
|
||||
|
||||
const REPORT_TYPE_LABELS = {
|
||||
@@ -161,28 +202,79 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
||||
|
||||
const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []);
|
||||
|
||||
// Fetch the reported content for each report
|
||||
const reportsWithContent = await Promise.all(
|
||||
(data || []).map(async (report) => {
|
||||
let reportedContent = null;
|
||||
|
||||
if (report.reported_entity_type === 'review') {
|
||||
const { data: reviewData } = await supabase
|
||||
// Batch fetch reported content to avoid N+1 queries
|
||||
// Separate entity IDs by type for efficient batching
|
||||
const reviewIds = data?.filter(r => r.reported_entity_type === 'review')
|
||||
.map(r => r.reported_entity_id) || [];
|
||||
const profileIds = data?.filter(r => r.reported_entity_type === 'profile')
|
||||
.map(r => r.reported_entity_id) || [];
|
||||
const submissionIds = data?.filter(r => r.reported_entity_type === 'content_submission')
|
||||
.map(r => r.reported_entity_id) || [];
|
||||
|
||||
// Parallel batch fetch for all entity types
|
||||
const [reviewsData, profilesData, submissionsData] = await Promise.all([
|
||||
reviewIds.length > 0
|
||||
? supabase
|
||||
.from('reviews')
|
||||
.select('title, content, rating')
|
||||
.eq('id', report.reported_entity_id)
|
||||
.single();
|
||||
reportedContent = reviewData;
|
||||
}
|
||||
// Add other entity types as needed
|
||||
|
||||
return {
|
||||
...report,
|
||||
reporter_profile: profileMap.get(report.reporter_id),
|
||||
reported_content: reportedContent,
|
||||
};
|
||||
})
|
||||
.select('id, title, content, rating')
|
||||
.in('id', reviewIds)
|
||||
.then(({ data }) => data || [])
|
||||
: Promise.resolve([]),
|
||||
|
||||
profileIds.length > 0
|
||||
? supabase
|
||||
.from('profiles')
|
||||
.select('user_id, username, display_name')
|
||||
.in('user_id', profileIds)
|
||||
.then(({ data }) => data || [])
|
||||
: Promise.resolve([]),
|
||||
|
||||
submissionIds.length > 0
|
||||
? supabase
|
||||
.from('content_submissions')
|
||||
.select('id, submission_type, status')
|
||||
.in('id', submissionIds)
|
||||
.then(({ data }) => data || [])
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
// Create lookup maps for O(1) access
|
||||
const reviewMap = new Map(
|
||||
reviewsData.map(r => [r.id, r as ReportedReview] as const)
|
||||
);
|
||||
const profilesMap = new Map(
|
||||
profilesData.map(p => [p.user_id, p as ReportedProfile] as const)
|
||||
);
|
||||
const submissionsMap = new Map(
|
||||
submissionsData.map(s => [s.id, s as ReportedSubmission] as const)
|
||||
);
|
||||
|
||||
// Map reports to their content (O(n) instead of O(n*m))
|
||||
const reportsWithContent: Report[] = (data || []).map(report => {
|
||||
let reportedContent: ReportedContent = null;
|
||||
|
||||
// Type-safe entity type handling
|
||||
const entityType = report.reported_entity_type as ReportEntityType;
|
||||
|
||||
switch (entityType) {
|
||||
case 'review':
|
||||
reportedContent = reviewMap.get(report.reported_entity_id) || null;
|
||||
break;
|
||||
case 'profile':
|
||||
reportedContent = profilesMap.get(report.reported_entity_id) || null;
|
||||
break;
|
||||
case 'content_submission':
|
||||
reportedContent = submissionsMap.get(report.reported_entity_id) || null;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...report,
|
||||
reported_entity_type: entityType,
|
||||
reporter_profile: profileMap.get(report.reporter_id),
|
||||
reported_content: reportedContent,
|
||||
};
|
||||
});
|
||||
|
||||
// Use smart merging for silent refreshes if strategy is 'merge'
|
||||
if (silent && refreshStrategy === 'merge') {
|
||||
@@ -474,7 +566,7 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
||||
<div>
|
||||
<Label>Reported Content:</Label>
|
||||
<div className="bg-destructive/5 border border-destructive/20 p-4 rounded-lg mt-1">
|
||||
{report.reported_entity_type === 'review' && (
|
||||
{report.reported_entity_type === 'review' && isReportedReview(report.reported_content) && (
|
||||
<div>
|
||||
{report.reported_content.title && (
|
||||
<h4 className="font-semibold mb-2">{report.reported_content.title}</h4>
|
||||
@@ -487,6 +579,28 @@ export const ReportsQueue = forwardRef<ReportsQueueRef>((props, ref) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.reported_entity_type === 'profile' && isReportedProfile(report.reported_content) && (
|
||||
<div>
|
||||
<div className="font-semibold mb-2">
|
||||
{report.reported_content.display_name || report.reported_content.username}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
@{report.reported_content.username}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.reported_entity_type === 'content_submission' && isReportedSubmission(report.reported_content) && (
|
||||
<div>
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold">Type:</span> {report.reported_content.submission_type}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<span className="font-semibold">Status:</span> {report.reported_content.status}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,30 @@ import { supabase } from '@/integrations/supabase/client';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useUserRole } from '@/hooks/useUserRole';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
// Type-safe role definitions
|
||||
const VALID_ROLES = ['admin', 'moderator', 'user'] as const;
|
||||
type ValidRole = typeof VALID_ROLES[number];
|
||||
|
||||
/**
|
||||
* Type guard to validate role strings
|
||||
* Prevents unsafe casting and ensures type safety
|
||||
*/
|
||||
function isValidRole(role: string): role is ValidRole {
|
||||
return VALID_ROLES.includes(role as ValidRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display label for a role
|
||||
*/
|
||||
function getRoleLabel(role: string): string {
|
||||
const labels: Record<ValidRole, string> = {
|
||||
admin: 'Administrator',
|
||||
moderator: 'Moderator',
|
||||
user: 'User',
|
||||
};
|
||||
return isValidRole(role) ? labels[role] : role;
|
||||
}
|
||||
interface UserRole {
|
||||
id: string;
|
||||
user_id: string;
|
||||
@@ -109,8 +133,19 @@ export function UserRoleManager() {
|
||||
}, 300);
|
||||
return () => clearTimeout(debounceTimer);
|
||||
}, [newUserSearch, userRoles]);
|
||||
const grantRole = async (userId: string, role: 'admin' | 'moderator' | 'user') => {
|
||||
const grantRole = async (userId: string, role: ValidRole) => {
|
||||
if (!isAdmin()) return;
|
||||
|
||||
// Double-check role validity before database operation
|
||||
if (!isValidRole(role)) {
|
||||
toast({
|
||||
title: "Invalid Role",
|
||||
description: "The selected role is not valid",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoading('grant');
|
||||
try {
|
||||
const {
|
||||
@@ -120,10 +155,12 @@ export function UserRoleManager() {
|
||||
role,
|
||||
granted_by: user?.id
|
||||
}]);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: "Role Granted",
|
||||
description: `User has been granted ${role} role`
|
||||
description: `User has been granted ${getRoleLabel(role)} role`
|
||||
});
|
||||
setNewUserSearch('');
|
||||
setNewRole('');
|
||||
@@ -223,10 +260,20 @@ export function UserRoleManager() {
|
||||
|
||||
<Button onClick={() => {
|
||||
const selectedUser = searchResults.find(p => (p.display_name || p.username) === newUserSearch);
|
||||
if (selectedUser && newRole) {
|
||||
grantRole(selectedUser.user_id, newRole as 'admin' | 'moderator' | 'user');
|
||||
|
||||
// Type-safe validation before calling grantRole
|
||||
if (selectedUser && newRole && isValidRole(newRole)) {
|
||||
grantRole(selectedUser.user_id, newRole);
|
||||
} else if (selectedUser && newRole) {
|
||||
// This should never happen due to Select component constraints,
|
||||
// but provides safety in case of UI bugs
|
||||
toast({
|
||||
title: "Invalid Role",
|
||||
description: "Please select a valid role",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}} disabled={!newRole || !searchResults.find(p => (p.display_name || p.username) === newUserSearch) || actionLoading === 'grant'} className="w-full md:w-auto">
|
||||
}} disabled={!newRole || !isValidRole(newRole) || !searchResults.find(p => (p.display_name || p.username) === newUserSearch) || actionLoading === 'grant'} className="w-full md:w-auto">
|
||||
{actionLoading === 'grant' ? 'Granting...' : 'Grant Role'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user