Approve tool use

This commit is contained in:
gpt-engineer-app[bot]
2025-11-02 21:46:47 +00:00
parent f81037488c
commit a9644c0bee
11 changed files with 2158 additions and 18 deletions

View File

@@ -12,6 +12,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
import { UserAvatar } from '@/components/ui/user-avatar';
import { format } from 'date-fns';
import type { ModerationItem } from '@/types/moderation';
import { sanitizeURL, sanitizePlainText } from '@/lib/sanitize';
interface QueueItemActionsProps {
item: ModerationItem;
@@ -166,12 +167,12 @@ export const QueueItemActions = memo(({
<div className="text-sm">
<span className="font-medium text-blue-900 dark:text-blue-100">Source: </span>
<a
href={item.submission_items[0].item_data.source_url}
href={sanitizeURL(item.submission_items[0].item_data.source_url)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline dark:text-blue-400 inline-flex items-center gap-1"
>
{item.submission_items[0].item_data.source_url}
{sanitizePlainText(item.submission_items[0].item_data.source_url)}
<ExternalLink className="w-3 h-3" />
</a>
</div>
@@ -181,7 +182,7 @@ export const QueueItemActions = memo(({
<div className="text-sm">
<span className="font-medium text-blue-900 dark:text-blue-100">Submitter Notes: </span>
<p className="mt-1 whitespace-pre-wrap text-blue-800 dark:text-blue-200">
{item.submission_items[0].item_data.submission_notes}
{sanitizePlainText(item.submission_items[0].item_data.submission_notes)}
</p>
</div>
)}
@@ -366,12 +367,12 @@ export const QueueItemActions = memo(({
<div className="text-sm mb-2">
<span className="font-medium">Source: </span>
<a
href={item.submission_items[0].item_data.source_url}
href={sanitizeURL(item.submission_items[0].item_data.source_url)}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
{item.submission_items[0].item_data.source_url}
{sanitizePlainText(item.submission_items[0].item_data.source_url)}
<ExternalLink className="w-3 h-3" />
</a>
</div>
@@ -380,7 +381,7 @@ export const QueueItemActions = memo(({
<div className="text-sm">
<span className="font-medium">Submitter Notes: </span>
<p className="mt-1 whitespace-pre-wrap text-muted-foreground">
{item.submission_items[0].item_data.submission_notes}
{sanitizePlainText(item.submission_items[0].item_data.submission_notes)}
</p>
</div>
)}

View File

@@ -1198,6 +1198,53 @@ export type Database = {
}
Relationships: []
}
moderation_audit_log: {
Row: {
action: string
created_at: string
id: string
is_test_data: boolean | null
metadata: Json | null
moderator_id: string
new_status: string | null
notes: string | null
previous_status: string | null
submission_id: string | null
}
Insert: {
action: string
created_at?: string
id?: string
is_test_data?: boolean | null
metadata?: Json | null
moderator_id: string
new_status?: string | null
notes?: string | null
previous_status?: string | null
submission_id?: string | null
}
Update: {
action?: string
created_at?: string
id?: string
is_test_data?: boolean | null
metadata?: Json | null
moderator_id?: string
new_status?: string | null
notes?: string | null
previous_status?: string | null
submission_id?: string | null
}
Relationships: [
{
foreignKeyName: "moderation_audit_log_submission_id_fkey"
columns: ["submission_id"]
isOneToOne: false
referencedRelation: "content_submissions"
referencedColumns: ["id"]
},
]
}
notification_channels: {
Row: {
channel_type: string
@@ -4708,6 +4755,17 @@ export type Database = {
Returns: undefined
}
log_cleanup_results: { Args: never; Returns: undefined }
log_moderation_action: {
Args: {
_action: string
_metadata?: Json
_new_status?: string
_notes?: string
_previous_status?: string
_submission_id: string
}
Returns: string
}
log_request_metadata: {
Args: {
p_client_version?: string
@@ -4788,6 +4846,10 @@ export type Database = {
Args: { target_ride_id: string }
Returns: undefined
}
validate_moderation_action: {
Args: { _action: string; _submission_id: string; _user_id: string }
Returns: boolean
}
}
Enums: {
account_deletion_status:

98
src/lib/sanitize.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Input Sanitization Utilities
*
* Provides XSS protection for user-generated content.
* All user input should be sanitized before rendering to prevent injection attacks.
*/
import DOMPurify from 'dompurify';
/**
* Sanitize HTML content to prevent XSS attacks
*
* @param html - Raw HTML string from user input
* @returns Sanitized HTML safe for rendering
*/
export function sanitizeHTML(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
ALLOW_DATA_ATTR: false,
});
}
/**
* Sanitize URL to prevent javascript: and data: protocol injection
*
* @param url - URL from user input
* @returns Sanitized URL or '#' if invalid
*/
export function sanitizeURL(url: string): string {
if (!url || typeof url !== 'string') {
return '#';
}
try {
const parsed = new URL(url);
// Only allow http, https, and mailto protocols
const allowedProtocols = ['http:', 'https:', 'mailto:'];
if (!allowedProtocols.includes(parsed.protocol)) {
console.warn(`Blocked potentially dangerous URL protocol: ${parsed.protocol}`);
return '#';
}
return url;
} catch {
// Invalid URL format
console.warn(`Invalid URL format: ${url}`);
return '#';
}
}
/**
* Sanitize plain text to prevent any HTML rendering
* Escapes all HTML entities
*
* @param text - Plain text from user input
* @returns Escaped text safe for rendering
*/
export function sanitizePlainText(text: string): string {
if (!text || typeof text !== 'string') {
return '';
}
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
/**
* Check if a string contains potentially dangerous content
* Used for validation before sanitization
*
* @param input - User input to check
* @returns true if input contains suspicious patterns
*/
export function containsSuspiciousContent(input: string): boolean {
if (!input || typeof input !== 'string') {
return false;
}
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i, // Event handlers like onclick=
/<iframe/i,
/<object/i,
/<embed/i,
/data:text\/html/i,
];
return suspiciousPatterns.some(pattern => pattern.test(input));
}