-
Notifications
You must be signed in to change notification settings - Fork 24
Feat/kyc tier setup #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d8c5ff0
6d6fe4c
45d4199
051f639
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| 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 }); | ||
| } | ||
| } | ||
| 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) { | ||
| try { | ||
| const terms = await TermsService.getCurrentTermsVersion(); | ||
| return NextResponse.json(terms); | ||
| } catch (error) { | ||
| 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, | ||
| }); | ||
|
|
||
| return NextResponse.json(acceptance); | ||
| } catch (error) { | ||
| console.error("Error accepting terms:", error); | ||
| return NextResponse.json({ error: "Failed to accept terms" }, { status: 500 }); | ||
| } | ||
| } | ||
| 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) { | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate 💡 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 |
||
| } | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| 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) { | ||
| return NextResponse.json({ error: "Failed to fetch verification status" }, { status: 500 }); | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate submission payload and normalize client IP.
🔧 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json(withdrawal); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error: any) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Error submitting withdrawal:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: error.message || "Withdrawal failed" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate request body and normalize client IP before 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 |
||
|
|
||
| return NextResponse.json(validation); | ||
| } catch (error) { | ||
| console.error("Error validating withdrawal:", error); | ||
| return NextResponse.json({ error: "Validation failed" }, { status: 500 }); | ||
| } | ||
| } | ||
| 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) { | ||
| 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> | ||
| ); | ||
| } | ||
| 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) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(err.message || "Upload failed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUploading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clear invalid selections so re-uploads work reliably. 🛠️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 99
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 266
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 251
🏁 Script executed:
# Read the file to see the actual code cat -n ./app/api/compliance/terms/route.tsRepository: boundlessfi/bounties
Length of output: 1549
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 1216
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 894
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 600
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 1032
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 46
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 385
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 80
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 1620
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 245
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 175
Harden IP capture from
x-forwarded-foracross 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, andwithdrawal/submit. Parse the first hop and ensure this header is trusted by your proxy config.🔧 Suggested adjustment
🤖 Prompt for AI Agents