Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/api/compliance/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/server-auth";
import { ComplianceService } from "@/lib/services/compliance";
import { TermsService } from "@/lib/services/terms";

export async function GET(request: NextRequest) {

Check warning on line 6 in app/api/compliance/status/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'request' is defined but never used
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const compliance = await ComplianceService.getUserCompliance(user.id);
const remaining = await ComplianceService.getRemainingLimits(user.id);
const termsStatus = await TermsService.getUserTermsStatus(user.id);
const nextTier = ComplianceService.getNextTier(compliance.currentTier);

return NextResponse.json({
compliance,
remaining,
termsStatus,
nextTier,
});
} catch (error) {
console.error("Error fetching compliance status:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}
35 changes: 35 additions & 0 deletions app/api/compliance/terms/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/server-auth";
import { TermsService } from "@/lib/services/terms";

export async function GET(request: NextRequest) {

Check warning on line 5 in app/api/compliance/terms/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'request' is defined but never used
try {
const terms = await TermsService.getCurrentTermsVersion();
return NextResponse.json(terms);
} catch (error) {

Check warning on line 9 in app/api/compliance/terms/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'error' is defined but never used
return NextResponse.json({ error: "Failed to fetch terms" }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { termsVersionId } = await request.json();
const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
const userAgent = request.headers.get('user-agent') || 'unknown';

const acceptance = await TermsService.acceptTerms(user.id, termsVersionId, {
ipAddress: ip,
userAgent,
Comment on lines +21 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -name "route.ts" -path "*/compliance/terms/*" 2>/dev/null | head -5

Repository: boundlessfi/bounties

Length of output: 99


🏁 Script executed:

# Get the file structure to understand the project layout
ls -la app/api/compliance/terms/ 2>/dev/null || echo "Path not found, searching for the file..."
git ls-files | grep -i "compliance.*terms.*route"

Repository: boundlessfi/bounties

Length of output: 266


🏁 Script executed:

# Search for the specific file more broadly
find . -type f -name "*.ts" | grep -i "compliance" | head -10

Repository: boundlessfi/bounties

Length of output: 251


🏁 Script executed:

# Read the file to see the actual code
cat -n ./app/api/compliance/terms/route.ts

Repository: boundlessfi/bounties

Length of output: 1549


🏁 Script executed:

# Search for other x-forwarded-for usage in the codebase
rg "x-forwarded-for" -i -A 2 -B 2

Repository: boundlessfi/bounties

Length of output: 1216


🏁 Script executed:

# Search for general IP header handling patterns
rg "request\.headers" -A 1 -B 1 | head -50

Repository: boundlessfi/bounties

Length of output: 894


🏁 Script executed:

# Check for middleware or utility functions that might handle x-forwarded-for
rg "x-forwarded-for|getClientIp|parseForwarded|ipAddress" -i --type ts --type js | grep -v node_modules | head -30

Repository: boundlessfi/bounties

Length of output: 600


🏁 Script executed:

# Look at the package.json to understand the framework version
cat package.json | grep -A 5 -B 5 "next"

Repository: boundlessfi/bounties

Length of output: 1032


🏁 Script executed:

# Search for any IP utility or helper functions
find . -type f \( -name "*ip*" -o -name "*address*" -o -name "*header*" \) -name "*.ts" 2>/dev/null | grep -E "(lib|utils|helpers)" | head -10

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

# Check if there's any middleware or configuration that validates x-forwarded-for
rg "x-forwarded-for|trusted.*proxy|proxy.*config" -i --type ts --type js | grep -v node_modules

Repository: boundlessfi/bounties

Length of output: 385


🏁 Script executed:

# Look at next.config or any server configuration
find . -maxdepth 2 -name "next.config*" -o -name "middleware.ts" -o -name "*.config.ts" 2>/dev/null

Repository: boundlessfi/bounties

Length of output: 80


🏁 Script executed:

# Check the services to understand how ipAddress is used in audit context
cat -n lib/services/terms.ts | grep -A 10 -B 10 "ipAddress"

Repository: boundlessfi/bounties

Length of output: 1620


🏁 Script executed:

# Check next.config.ts to see if there's any proxy trust configuration
cat -n ./next.config.ts | head -40

Repository: boundlessfi/bounties

Length of output: 245


🏁 Script executed:

# Get a count of how many endpoints have this issue
rg "request\.headers\.get\('x-forwarded-for'\)" --count-matches

Repository: boundlessfi/bounties

Length of output: 175


Harden IP capture from x-forwarded-for across multiple endpoints.

The entire header is stored without parsing the first hop, which may contain multiple comma-separated IPs or spoofed values. This affects audit trail integrity in three endpoints: compliance/terms, withdrawal/validate, and withdrawal/submit. Parse the first hop and ensure this header is trusted by your proxy config.

🔧 Suggested adjustment
-        const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
+        const forwardedFor = request.headers.get('x-forwarded-for');
+        const ip = forwardedFor?.split(',')[0]?.trim() || '0.0.0.0';
🤖 Prompt for AI Agents
In `@app/api/compliance/terms/route.ts` around lines 21 - 27, The code is storing
the full x-forwarded-for header value in ip (from
request.headers.get('x-forwarded-for')) which may contain multiple
comma-separated IPs; update the extraction logic used before calling
TermsService.acceptTerms (and mirror the same change in the withdrawal/validate
and withdrawal/submit endpoints) to parse the header, split on commas, trim and
take the first non-empty token as the client IP (fall back to a safe default
like '0.0.0.0' if none), keep userAgent extraction unchanged, and ensure
comments note that the proxy must be configured to set/trust X-Forwarded-For.

});

return NextResponse.json(acceptance);
} catch (error) {
console.error("Error accepting terms:", error);
return NextResponse.json({ error: "Failed to accept terms" }, { status: 500 });
}
}
41 changes: 41 additions & 0 deletions app/api/compliance/upgrade/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/server-auth";
import { VerificationService } from "@/lib/services/verification";

export async function POST(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { targetTier } = await request.json();

const verificationRequest = await VerificationService.createVerificationRequest(
user.id,
targetTier
);

return NextResponse.json(verificationRequest);
} catch (error: any) {

Check failure on line 20 in app/api/compliance/upgrade/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

Unexpected any. Specify a different type
console.error("Error creating verification request:", error);
return NextResponse.json(
{ error: error.message || "Failed to create verification request" },
{ status: 400 }
);
}
Comment on lines +5 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate targetTier early to return clear 400s.
If the body is missing or malformed, VerificationService may throw a generic error. An explicit guard gives cleaner client feedback.

💡 Suggested fix
-        const { targetTier } = await request.json();
+        const body = await request.json();
+        const { targetTier } = body ?? {};
+        if (!targetTier) {
+            return NextResponse.json({ error: "targetTier is required" }, { status: 400 });
+        }
🤖 Prompt for AI Agents
In `@app/api/compliance/upgrade/route.ts` around lines 5 - 26, The POST handler
doesn't validate the incoming body and targetTier which can lead to generic
errors; update the POST function to parse and validate request.json() early
(ensure body exists and that targetTier is present and of the expected
type/value) before calling VerificationService.createVerificationRequest, and
return a clear NextResponse.json({ error: "Invalid request: targetTier is
required" }, { status: 400 }) for bad input; keep the existing getCurrentUser
check and error catch but perform this guard right after parsing the body to
provide explicit 400 responses.

}

export async function GET(request: NextRequest) {

Check warning on line 29 in app/api/compliance/upgrade/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'request' is defined but never used
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const status = await VerificationService.getVerificationStatus(user.id);
return NextResponse.json(status);
} catch (error) {

Check warning on line 38 in app/api/compliance/upgrade/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'error' is defined but never used
return NextResponse.json({ error: "Failed to fetch verification status" }, { status: 500 });
}
}
31 changes: 31 additions & 0 deletions app/api/withdrawal/submit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/server-auth";
import { WithdrawalService } from "@/lib/services/withdrawal";

export async function POST(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { amount, currency, destinationId } = await request.json();
const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';

const withdrawal = await WithdrawalService.submit(
user.id,
amount,
currency,
destinationId,
ip
);
Comment on lines +12 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate submission payload and normalize client IP.

amount, currency, and destinationId are unvalidated; malformed payloads can reach the service. Also, normalize x-forwarded-for before geo checks.

🔧 Proposed fix (payload validation + IP normalization)
-        const { amount, currency, destinationId } = await request.json();
-        const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
+        const { amount, currency, destinationId } = await request.json();
+        if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
+            return NextResponse.json({ error: "Invalid amount" }, { status: 400 });
+        }
+        if (typeof currency !== "string" || !currency.trim()) {
+            return NextResponse.json({ error: "Invalid currency" }, { status: 400 });
+        }
+        if (typeof destinationId !== "string" || !destinationId.trim()) {
+            return NextResponse.json({ error: "Invalid destination" }, { status: 400 });
+        }
+        const forwardedFor = request.headers.get("x-forwarded-for");
+        const ip = forwardedFor?.split(",")[0]?.trim() || "0.0.0.0";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { amount, currency, destinationId } = await request.json();
const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
const withdrawal = await WithdrawalService.submit(
user.id,
amount,
currency,
destinationId,
ip
);
const { amount, currency, destinationId } = await request.json();
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
return NextResponse.json({ error: "Invalid amount" }, { status: 400 });
}
if (typeof currency !== "string" || !currency.trim()) {
return NextResponse.json({ error: "Invalid currency" }, { status: 400 });
}
if (typeof destinationId !== "string" || !destinationId.trim()) {
return NextResponse.json({ error: "Invalid destination" }, { status: 400 });
}
const forwardedFor = request.headers.get("x-forwarded-for");
const ip = forwardedFor?.split(",")[0]?.trim() || "0.0.0.0";
const withdrawal = await WithdrawalService.submit(
user.id,
amount,
currency,
destinationId,
ip
);
🤖 Prompt for AI Agents
In `@app/api/withdrawal/submit/route.ts` around lines 12 - 21, Validate and
sanitize the incoming payload before calling WithdrawalService.submit: parse and
assert that amount is a positive number (e.g., Number(amount) and > 0), that
currency matches your allowed ISO/currency regex or whitelist, and that
destinationId is present and a valid ID format; if validation fails, return a
4xx response. Normalize the client IP from
request.headers.get('x-forwarded-for') by splitting on commas, taking the first
non-empty value and trimming it (fallback to '0.0.0.0' only if none), and pass
that normalized ip into WithdrawalService.submit. Ensure you perform these
checks where request.json() is consumed and only call
WithdrawalService.submit(user.id, amount, currency, destinationId, ip) with the
validated/normalized values.


return NextResponse.json(withdrawal);
} catch (error: any) {

Check failure on line 24 in app/api/withdrawal/submit/route.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

Unexpected any. Specify a different type
console.error("Error submitting withdrawal:", error);
return NextResponse.json(
{ error: error.message || "Withdrawal failed" },
{ status: 400 }
);
}
}
22 changes: 22 additions & 0 deletions app/api/withdrawal/validate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/server-auth";
import { WithdrawalService } from "@/lib/services/withdrawal";

export async function POST(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { amount } = await request.json();
const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';

const validation = await WithdrawalService.validate(user.id, amount, ip);
Comment on lines +12 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate request body and normalize client IP before checks.

amount is unvalidated and x-forwarded-for can contain a comma‑separated list (or be spoofed). This can lead to NaN/invalid amounts passing through or incorrect geo checks.

🔧 Proposed fix (validation + IP normalization)
-        const { amount } = await request.json();
-        const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
+        const { amount } = await request.json();
+        if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) {
+            return NextResponse.json({ error: "Invalid amount" }, { status: 400 });
+        }
+        const forwardedFor = request.headers.get("x-forwarded-for");
+        const ip = forwardedFor?.split(",")[0]?.trim() || "0.0.0.0";
🤖 Prompt for AI Agents
In `@app/api/withdrawal/validate/route.ts` around lines 12 - 15, The handler
currently reads amount from request.json() and uses
request.headers.get('x-forwarded-for') directly before calling
WithdrawalService.validate(user.id, amount, ip); validate and normalize these
first: parse and validate amount (ensure it exists, is numeric, finite, positive
and within allowed bounds) and coerce it into the expected numeric type before
passing to WithdrawalService.validate, and normalize the client IP by taking the
first entry of the x-forwarded-for header (split on commas, trim) falling back
to request.connection/remoteAddress or '0.0.0.0' if absent; reject the request
early with a 400/appropriate error when validation fails rather than forwarding
invalid amount or malformed IP to WithdrawalService.validate.


return NextResponse.json(validation);
} catch (error) {
console.error("Error validating withdrawal:", error);
return NextResponse.json({ error: "Validation failed" }, { status: 500 });
}
}
4 changes: 2 additions & 2 deletions app/wallet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function WalletPage() {
return (
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 text-center lg:text-left">
<h1 className="text-3xl font-bold tracking-tight">Wallet</h1>
<p className="text-muted-foreground">
Manage your earnings, assets, and withdrawals.
</p>
</div>

<div className="grid gap-8 lg:grid-cols-3">
<div className="grid gap-8 lg:grid-cols-3 justify-items-center lg:justify-items-stretch">
<div className="lg:col-span-2 space-y-8">
<BalanceCard walletInfo={mockWalletWithAssets} />

Expand Down
96 changes: 96 additions & 0 deletions components/compliance/appeal-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";

import { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { AppealService } from "@/lib/services/appeal";

interface AppealDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
verificationRequestId: string;
userId: string;
rejectionReason?: string;
}

export function AppealDialog({ open, onOpenChange, verificationRequestId, userId, rejectionReason }: AppealDialogProps) {
const [reason, setReason] = useState("");
const [additionalInfo, setAdditionalInfo] = useState("");
const [submitting, setSubmitting] = useState(false);

const handleSubmit = async () => {
if (!reason.trim()) return;

setSubmitting(true);
try {
await AppealService.submitAppeal(userId, verificationRequestId, reason, additionalInfo);
alert('Appeal submitted successfully. Our team will review it within 3-5 business days.');
onOpenChange(false);
setReason("");
setAdditionalInfo("");
} catch (error: any) {

Check failure on line 33 in components/compliance/appeal-dialog.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

Unexpected any. Specify a different type
alert(error.message || 'Failed to submit appeal');
} finally {
setSubmitting(false);
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Appeal Verification Decision</DialogTitle>
<DialogDescription>
Explain why you believe the rejection should be reconsidered.
</DialogDescription>
</DialogHeader>

<div className="space-y-4">
{rejectionReason && (
<div className="bg-muted/50 rounded-lg p-3">
<p className="text-sm font-medium mb-1">Original Rejection Reason:</p>
<p className="text-sm text-muted-foreground">{rejectionReason}</p>
</div>
)}

<div className="space-y-2">
<Label htmlFor="reason">Why should we reconsider?</Label>
<Textarea
id="reason"
placeholder="Explain your situation..."
className="min-h-[100px]"
value={reason}
onChange={(e) => setReason(e.target.value)}
required
/>
</div>

<div className="space-y-2">
<Label htmlFor="additionalInfo">Additional Information (Optional)</Label>
<Textarea
id="additionalInfo"
placeholder="Any additional context..."
className="min-h-[80px]"
value={additionalInfo}
onChange={(e) => setAdditionalInfo(e.target.value)}
/>
</div>
</div>

<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!reason.trim() || submitting}
>
{submitting ? 'Submitting...' : 'Submit Appeal'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
112 changes: 112 additions & 0 deletions components/compliance/document-upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Upload, X, FileText, CheckCircle } from "lucide-react";
import { DocumentType } from "@/types/compliance";

interface DocumentUploadProps {
type: DocumentType;
label: string;
onUpload: (file: File) => Promise<void>;
uploaded?: boolean;
}

const acceptedTypes: Record<DocumentType, string> = {
GOVERNMENT_ID: "image/*,.pdf",
PROOF_OF_ADDRESS: "image/*,.pdf",
SELFIE: "image/*",
ADDITIONAL: "image/*,.pdf",
};

export function DocumentUpload({ type, label, onUpload, uploaded }: DocumentUploadProps) {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (!selectedFile) return;

if (selectedFile.size > 10 * 1024 * 1024) {
setError("File too large. Max 10MB.");
return;
}

setFile(selectedFile);
setError(null);

setUploading(true);
try {
await onUpload(selectedFile);
} catch (err: any) {

Check failure on line 43 in components/compliance/document-upload.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

Unexpected any. Specify a different type
setError(err.message || "Upload failed");
} finally {
setUploading(false);
}
};
Comment on lines +28 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear invalid selections so re-uploads work reliably.
If a file is too large, the input value remains set, so re-selecting the same file won’t trigger onChange. Clearing the input (and file state) avoids that trap.

🛠️ Suggested fix
-    const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
-        const selectedFile = e.target.files?.[0];
+    const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
+        const input = e.currentTarget;
+        const selectedFile = input.files?.[0];
         if (!selectedFile) return;
 
         if (selectedFile.size > 10 * 1024 * 1024) {
             setError("File too large. Max 10MB.");
+            setFile(null);
+            input.value = "";
             return;
         }
@@
-        } finally {
+        } finally {
             setUploading(false);
+            input.value = "";
         }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (!selectedFile) return;
if (selectedFile.size > 10 * 1024 * 1024) {
setError("File too large. Max 10MB.");
return;
}
setFile(selectedFile);
setError(null);
setUploading(true);
try {
await onUpload(selectedFile);
} catch (err: any) {
setError(err.message || "Upload failed");
} finally {
setUploading(false);
}
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const selectedFile = input.files?.[0];
if (!selectedFile) return;
if (selectedFile.size > 10 * 1024 * 1024) {
setError("File too large. Max 10MB.");
setFile(null);
input.value = "";
return;
}
setFile(selectedFile);
setError(null);
setUploading(true);
try {
await onUpload(selectedFile);
} catch (err: any) {
setError(err.message || "Upload failed");
} finally {
setUploading(false);
input.value = "";
}
};
🤖 Prompt for AI Agents
In `@components/compliance/document-upload.tsx` around lines 28 - 48, The
handleFileChange handler leaves the file input value set when a selected file is
rejected (e.g., too large), preventing re-selecting the same file from
triggering onChange; update handleFileChange to clear both the component file
state (setFile) and the input element’s value when rejecting a file (e.g., on
size > 10MB) and also when an upload fails, then proceed to setError and
setUploading as before and still call onUpload for valid files. Ensure you
reference the input element (via ref or e.target.value = '') so the same
filename can be re-selected, and keep using setFile, setError, setUploading, and
onUpload in the remaining logic.


const clearFile = () => {
setFile(null);
setError(null);
};

return (
<div className="space-y-2">
<Label>{label}</Label>

{!file && !uploaded && (
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer hover:bg-muted/50 transition-colors">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload className="w-8 h-8 mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Click to upload or drag and drop
</p>
<p className="text-xs text-muted-foreground mt-1">
PNG, JPG, PDF (max 10MB)
</p>
</div>
<input
type="file"
className="hidden"
accept={acceptedTypes[type]}
onChange={handleFileChange}
disabled={uploading}
/>
</label>
)}

{(file || uploaded) && (
<div className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-2">
{uploaded ? (
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<FileText className="w-5 h-5" />
)}
<div className="text-sm">
<p className="font-medium">{file?.name || "Document uploaded"}</p>
<p className="text-xs text-muted-foreground">
{uploaded ? "Verified" : uploading ? "Uploading..." : "Ready"}
</p>
</div>
</div>
{!uploaded && !uploading && (
<Button
variant="ghost"
size="sm"
onClick={clearFile}
>
<X className="w-4 h-4" />
</Button>
)}
</div>
)}

{error && (
<p className="text-xs text-destructive">{error}</p>
)}
</div>
);
}
Loading
Loading