diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..acc109c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +npx lint-staged --concurrent false diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..3319b29 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +npm run build diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 0000000..681f620 --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,4 @@ +{ + "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], + "*.{json,md,css}": ["prettier --write"] +} diff --git a/app/api/compliance/status/route.ts b/app/api/compliance/status/route.ts index 9054ac5..0c30e01 100644 --- a/app/api/compliance/status/route.ts +++ b/app/api/compliance/status/route.ts @@ -1,28 +1,31 @@ -import { NextRequest, NextResponse } from "next/server"; +import { 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 }); - } +export async function GET() { + 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); + 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 }); - } + 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 }, + ); + } } diff --git a/app/api/compliance/terms/route.ts b/app/api/compliance/terms/route.ts index 22ab0e5..a0b47b6 100644 --- a/app/api/compliance/terms/route.ts +++ b/app/api/compliance/terms/route.ts @@ -2,34 +2,51 @@ 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 GET() { + try { + const terms = await TermsService.getCurrentTermsVersion(); + return NextResponse.json(terms); + } catch (error) { + console.error("Failed to fetch terms:", 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'; + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } - const acceptance = await TermsService.acceptTerms(user.id, termsVersionId, { - ipAddress: ip, - userAgent, - }); + const { termsVersionId } = await request.json(); - return NextResponse.json(acceptance); - } catch (error) { - console.error("Error accepting terms:", error); - return NextResponse.json({ error: "Failed to accept terms" }, { status: 500 }); + // Validate termsVersionId + if (!termsVersionId || typeof termsVersionId !== "string") { + return NextResponse.json( + { error: "Invalid or missing termsVersionId" }, + { status: 400 }, + ); } + + const forwardedFor = request.headers.get("x-forwarded-for"); + const ip = forwardedFor ? forwardedFor.split(",")[0].trim() : "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 }, + ); + } } diff --git a/app/api/compliance/upgrade/route.ts b/app/api/compliance/upgrade/route.ts index c54ad2e..d087edf 100644 --- a/app/api/compliance/upgrade/route.ts +++ b/app/api/compliance/upgrade/route.ts @@ -1,41 +1,67 @@ import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/server-auth"; import { VerificationService } from "@/lib/services/verification"; +import { ComplianceService } from "@/lib/services/compliance"; +import { KYCTier } from "@/types/compliance"; 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 } - ); + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + + const { targetTier } = await request.json(); + + const VALID_TIERS = Object.keys(ComplianceService.TIER_CONFIGS).filter( + (t) => t !== "UNVERIFIED", + ) as KYCTier[]; + + // Validate targetTier + if (!targetTier || !VALID_TIERS.includes(targetTier as KYCTier)) { + return NextResponse.json( + { error: "Invalid or missing targetTier" }, + { status: 400 }, + ); + } + + const verificationRequest = + await VerificationService.createVerificationRequest(user.id, targetTier); + + return NextResponse.json(verificationRequest); + } catch (error) { + console.error("Error creating verification request:", error); + + const isValidationError = + error instanceof Error && + (error.name === "ValidationError" || + error.message.includes("Invalid") || + error.message.includes("not allowed")); + + return NextResponse.json( + { + error: + (error as Error).message || "Failed to create verification request", + }, + { status: isValidationError ? 400 : 500 }, + ); + } } -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 }); +export async function GET() { + 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) { + console.error("Failed to fetch verification status:", error); + return NextResponse.json( + { error: "Failed to fetch status" }, + { status: 500 }, + ); + } } diff --git a/app/api/withdrawal/submit/route.ts b/app/api/withdrawal/submit/route.ts index ce43753..b7aa0da 100644 --- a/app/api/withdrawal/submit/route.ts +++ b/app/api/withdrawal/submit/route.ts @@ -3,29 +3,59 @@ 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 - ); - - return NextResponse.json(withdrawal); - } catch (error: any) { - console.error("Error submitting withdrawal:", error); - return NextResponse.json( - { error: error.message || "Withdrawal failed" }, - { status: 400 } - ); + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + + const { amount, currency, destinationId } = await request.json(); + + // Input Validation + if (typeof amount !== "number" || !isFinite(amount) || amount <= 0) { + return NextResponse.json({ error: "Invalid amount" }, { status: 400 }); + } + if (!currency || typeof currency !== "string") { + return NextResponse.json({ error: "Invalid currency" }, { status: 400 }); + } + if (!destinationId || typeof destinationId !== "string") { + return NextResponse.json( + { error: "Invalid destinationId" }, + { status: 400 }, + ); + } + + const forwardedFor = request.headers.get("x-forwarded-for"); + const ip = forwardedFor ? forwardedFor.split(",")[0].trim() : "0.0.0.0"; + + const withdrawal = await WithdrawalService.submit( + user.id, + amount, + currency, + destinationId, + ip, + ); + + return NextResponse.json(withdrawal); + } catch (error) { + console.error("Error submitting withdrawal:", error); + + let message = "Withdrawal failed"; + let status = 500; // Default to 500 + + if (error instanceof Error) { + message = error.message; + const err = error as Error & { status?: number; statusCode?: number }; + status = + err.status || + err.statusCode || + (error.name === "ValidationError" || error.name === "BadRequestError" + ? 400 + : 500); + } else { + message = String(error); + } + + return NextResponse.json({ error: message }, { status }); + } } diff --git a/components/compliance/appeal-dialog.tsx b/components/compliance/appeal-dialog.tsx index 3cbbcd1..9da687b 100644 --- a/components/compliance/appeal-dialog.tsx +++ b/components/compliance/appeal-dialog.tsx @@ -1,96 +1,120 @@ "use client"; import { useState } from "react"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +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; + 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); +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; + 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); - } - }; + 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) { + alert((error as Error).message || "Failed to submit appeal"); + } finally { + setSubmitting(false); + } + }; - return ( - - - - Appeal Verification Decision - - Explain why you believe the rejection should be reconsidered. - - + return ( + + + + Appeal Verification Decision + + Explain why you believe the rejection should be reconsidered. + + -
- {rejectionReason && ( -
-

Original Rejection Reason:

-

{rejectionReason}

-
- )} +
+ {rejectionReason && ( +
+

+ Original Rejection Reason: +

+

{rejectionReason}

+
+ )} -
- -