From 28ec17409f97fd65578e081551e5b0bcf22846fd Mon Sep 17 00:00:00 2001 From: talatops Date: Tue, 31 Mar 2026 18:16:30 +0500 Subject: [PATCH] fix: supplier order side & removed chat encryption --- .../migration.sql | 15 + prisma/schema.prisma | 11 +- .../3204a4a4-4580-44fa-8320-c0d3f2d93125.png | Bin 0 -> 19385 bytes .../40791d23-ead0-4ab9-b104-5f0a1b30d2b3.ico | Bin 0 -> 7645 bytes .../f266e026-e6c5-458e-87f0-1727a7c55ba1.png | Bin 0 -> 15077 bytes src/app/api/chat/contacts/route.ts | 5 - src/app/api/chat/messages/route.ts | 13 +- .../api/supplier/orders/[orderId]/route.ts | 216 +++++++++++ src/app/api/supplier/orders/route.ts | 6 + src/app/api/user/chat-key/route.ts | 53 --- src/app/api/user/chat-setup/route.ts | 45 --- src/app/supplier/orders/[orderId]/page.tsx | 348 ++++++++++++++++++ src/app/supplier/orders/page.tsx | 31 +- src/app/uploads/[...path]/route.ts | 56 +++ src/auth.supplier.ts | 5 +- src/auth.ts | 5 +- src/auth.vendor.ts | 5 +- src/components/chat/ChatView.tsx | 256 ++----------- src/hooks/useChatCrypto.ts | 97 ----- src/lib/chat-crypto.ts | 164 --------- src/types/next-auth.d.ts | 1 - 21 files changed, 712 insertions(+), 620 deletions(-) create mode 100644 prisma/migrations/20260330130000_chat_plaintext_cleanup/migration.sql create mode 100644 public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/3204a4a4-4580-44fa-8320-c0d3f2d93125.png create mode 100644 public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/40791d23-ead0-4ab9-b104-5f0a1b30d2b3.ico create mode 100644 public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/f266e026-e6c5-458e-87f0-1727a7c55ba1.png create mode 100644 src/app/api/supplier/orders/[orderId]/route.ts delete mode 100644 src/app/api/user/chat-key/route.ts delete mode 100644 src/app/api/user/chat-setup/route.ts create mode 100644 src/app/supplier/orders/[orderId]/page.tsx create mode 100644 src/app/uploads/[...path]/route.ts delete mode 100644 src/hooks/useChatCrypto.ts delete mode 100644 src/lib/chat-crypto.ts diff --git a/prisma/migrations/20260330130000_chat_plaintext_cleanup/migration.sql b/prisma/migrations/20260330130000_chat_plaintext_cleanup/migration.sql new file mode 100644 index 0000000..46d81d6 --- /dev/null +++ b/prisma/migrations/20260330130000_chat_plaintext_cleanup/migration.sql @@ -0,0 +1,15 @@ +-- Remove obsolete chat key columns from User. +ALTER TABLE "User" + DROP COLUMN IF EXISTS "chatPublicKey", + DROP COLUMN IF EXISTS "chatPrivateKeyEncrypted"; + +-- Rename encrypted message payload to plain content. +ALTER TABLE "ChatMessage" + RENAME COLUMN "encryptedContent" TO "content"; + +-- Remove obsolete encryption metadata columns. +ALTER TABLE "ChatMessage" + DROP COLUMN IF EXISTS "encryptedKeyForSender", + DROP COLUMN IF EXISTS "encryptedKeyForReceiver", + DROP COLUMN IF EXISTS "iv"; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 980bd3f..a2dd13b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,9 +27,6 @@ model User { mfaEnabled Boolean @default(false) mfaSecret String? // Encrypted TOTP secret - chatPublicKey String? - chatPrivateKeyEncrypted String? @db.Text - // Relations sentMessages ChatMessage[] @relation("SentMessages") receivedMessages ChatMessage[] @relation("ReceivedMessages") @@ -157,6 +154,9 @@ model CachedOrder { currency String? financialStatus String? fulfillmentStatus String? + trackingNumber String? + trackingCompany String? + trackingUrl String? lineItems Json // Store line items as JSON customer Json? // Store customer info as JSON createdAt DateTime @default(now()) @@ -559,10 +559,7 @@ model ChatMessage { id String @id @default(cuid()) senderId String receiverId String - encryptedContent String @db.Text - encryptedKeyForSender String @db.Text - encryptedKeyForReceiver String @db.Text - iv String + content String @db.Text sender User @relation("SentMessages", fields: [senderId], references: [id]) receiver User @relation("ReceivedMessages", fields: [receiverId], references: [id]) diff --git a/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/3204a4a4-4580-44fa-8320-c0d3f2d93125.png b/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/3204a4a4-4580-44fa-8320-c0d3f2d93125.png new file mode 100644 index 0000000000000000000000000000000000000000..5c52822a4de9d8c2565eef0b990d547d181daa53 GIT binary patch literal 19385 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 literal 0 HcmV?d00001 diff --git a/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/40791d23-ead0-4ab9-b104-5f0a1b30d2b3.ico b/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/40791d23-ead0-4ab9-b104-5f0a1b30d2b3.ico new file mode 100644 index 0000000000000000000000000000000000000000..dd5a12627d36db7eb9c19fa2f931ff1509f0323e GIT binary patch literal 7645 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 LNkvXXu0mjf>#GbK literal 0 HcmV?d00001 diff --git a/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/f266e026-e6c5-458e-87f0-1727a7c55ba1.png b/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/f266e026-e6c5-458e-87f0-1727a7c55ba1.png new file mode 100644 index 0000000000000000000000000000000000000000..b7671107979a3a191ef162c8fa94f523ae3f8eb7 GIT binary patch literal 15077 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 + for (const sync of acceptedSyncs) { + if (!syncsByStore.has(sync.storeId)) syncsByStore.set(sync.storeId, new Set()) + syncsByStore.get(sync.storeId)!.add(sync.shopifyProductId) + } + + return { profile, syncsByStore } +} + +function extractLineItems(lineItems: unknown): Array<{ product_id?: number }> { + try { + const parsed = typeof lineItems === "string" ? JSON.parse(lineItems) : lineItems + return Array.isArray(parsed) ? (parsed as Array<{ product_id?: number }>) : [] + } catch { + return [] + } +} + +function toObject(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : null +} + +function isSupplierAllowedForOrder( + syncsByStore: Map>, + storeId: string, + lineItems: unknown, +) { + const syncedProductIds = syncsByStore.get(storeId) + if (!syncedProductIds || syncedProductIds.size === 0) return false + const items = extractLineItems(lineItems) + return items.some((item) => { + const pid = item.product_id?.toString() + return !!pid && syncedProductIds.has(pid) + }) +} + +export async function GET( + _req: Request, + { params }: { params: Promise<{ orderId: string }> }, +) { + try { + const session = await auth() + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { orderId } = await params + const ctx = await getSupplierContext(session.user.id) + if (!ctx) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + const order = await prisma.cachedOrder.findFirst({ + where: { shopifyOrderId: orderId }, + include: { + store: { + select: { id: true, businessName: true, shopifyStoreName: true }, + }, + }, + }) + + if (!order) { + return NextResponse.json({ message: "Order not found" }, { status: 404 }) + } + + if (!isSupplierAllowedForOrder(ctx.syncsByStore, order.storeId, order.lineItems)) { + return NextResponse.json({ message: "Order not accessible" }, { status: 403 }) + } + + let liveOrder: Record | null = null + try { + const storeCredentials = await prisma.store.findUnique({ + where: { id: order.storeId }, + select: { shopifyDomain: true, shopifyAccessToken: true }, + }) + if (storeCredentials?.shopifyDomain && storeCredentials?.shopifyAccessToken) { + const cleanDomain = storeCredentials.shopifyDomain + .trim() + .replace(/^https?:\/\//, "") + .replace(/\/$/, "") + + const response = await fetch( + `https://${cleanDomain}/admin/api/2024-01/orders/${orderId}.json`, + { + method: "GET", + headers: { + "X-Shopify-Access-Token": storeCredentials.shopifyAccessToken, + "Content-Type": "application/json", + }, + }, + ) + if (response.ok) { + const payload = (await response.json()) as { order?: Record } + liveOrder = toObject(payload.order) + } + } + } catch (e) { + console.error("[SUPPLIER_ORDER_GET_SHOPIFY]", e) + } + + const cachedCustomer = toObject(order.customer) + const customer = toObject(liveOrder?.customer) || cachedCustomer + const shippingAddress = toObject(liveOrder?.shipping_address) + const billingAddress = toObject(liveOrder?.billing_address) + const lineItems = Array.isArray(liveOrder?.line_items) + ? (liveOrder?.line_items as Array<{ product_id?: number }>) + : extractLineItems(order.lineItems) + + return NextResponse.json({ + order: { + id: order.shopifyOrderId, + orderNumber: order.orderNumber, + email: (typeof liveOrder?.email === "string" && liveOrder.email) || order.email, + totalPrice: order.totalPrice, + currency: order.currency, + financialStatus: order.financialStatus, + fulfillmentStatus: order.fulfillmentStatus || "UNFULFILLED", + trackingNumber: (order as unknown as { trackingNumber?: string | null }).trackingNumber, + trackingCompany: (order as unknown as { trackingCompany?: string | null }).trackingCompany, + trackingUrl: (order as unknown as { trackingUrl?: string | null }).trackingUrl, + createdAt: order.shopifyCreatedAt || order.createdAt, + lineItems, + customer, + shippingAddress, + billingAddress, + store: order.store, + }, + }) + } catch (error) { + console.error("[SUPPLIER_ORDER_GET]", error) + return NextResponse.json({ message: "Failed to fetch order" }, { status: 500 }) + } +} + +export async function PATCH( + req: Request, + { params }: { params: Promise<{ orderId: string }> }, +) { + try { + const session = await auth() + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + const { orderId } = await params + const body = await req.json() + + const allowedStatuses = new Set(["UNFULFILLED", "IN_PROGRESS", "SHIPPED", "FULFILLED", "CANCELLED"]) + const fulfillmentStatus = String(body.fulfillmentStatus || "").toUpperCase() + if (!allowedStatuses.has(fulfillmentStatus)) { + return NextResponse.json({ message: "Invalid fulfillment status" }, { status: 400 }) + } + + const trackingNumber = String(body.trackingNumber || "").trim() || null + const trackingCompany = String(body.trackingCompany || "").trim() || null + const trackingUrl = String(body.trackingUrl || "").trim() || null + + const ctx = await getSupplierContext(session.user.id) + if (!ctx) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + const order = await prisma.cachedOrder.findFirst({ + where: { shopifyOrderId: orderId }, + select: { id: true, storeId: true, lineItems: true }, + }) + if (!order) return NextResponse.json({ message: "Order not found" }, { status: 404 }) + if (!isSupplierAllowedForOrder(ctx.syncsByStore, order.storeId, order.lineItems)) { + return NextResponse.json({ message: "Order not accessible" }, { status: 403 }) + } + + const updated = await prisma.cachedOrder.update({ + where: { id: order.id }, + data: { + fulfillmentStatus, + trackingNumber, + trackingCompany, + trackingUrl, + } as unknown as Record, + select: { + shopifyOrderId: true, + fulfillmentStatus: true, + }, + }) + + return NextResponse.json({ + order: { + ...updated, + trackingNumber, + trackingCompany, + trackingUrl, + }, + }) + } catch (error) { + console.error("[SUPPLIER_ORDER_PATCH]", error) + return NextResponse.json({ message: "Failed to update order" }, { status: 500 }) + } +} + diff --git a/src/app/api/supplier/orders/route.ts b/src/app/api/supplier/orders/route.ts index 7e95ddc..ac1ff37 100644 --- a/src/app/api/supplier/orders/route.ts +++ b/src/app/api/supplier/orders/route.ts @@ -151,9 +151,15 @@ export async function GET() { orderNumber: order.orderNumber, email: order.email, totalPrice: order.totalPrice, + total: Number.parseFloat(order.totalPrice || "0"), currency: order.currency, financialStatus: order.financialStatus, fulfillmentStatus: order.fulfillmentStatus, + status: order.fulfillmentStatus || "UNFULFILLED", + trackingNumber: (order as unknown as { trackingNumber?: string | null }).trackingNumber, + trackingCompany: (order as unknown as { trackingCompany?: string | null }).trackingCompany, + trackingUrl: (order as unknown as { trackingUrl?: string | null }).trackingUrl, + vendorName: order.store.businessName || order.store.shopifyStoreName, createdAt: order.shopifyCreatedAt || order.createdAt, store: order.store, lineItems: syncedLineItems, diff --git a/src/app/api/user/chat-key/route.ts b/src/app/api/user/chat-key/route.ts deleted file mode 100644 index 8179f3c..0000000 --- a/src/app/api/user/chat-key/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextResponse } from "next/server" -import { auth } from "@/auth" -import { prisma } from "@/lib/prisma" -import { decryptString } from "@/lib/encryption" - -export async function GET() { - try { - const session = await auth() - if (!session?.user?.id) { - return new NextResponse("Unauthorized", { status: 401 }) - } - - const user = await prisma.user.findUnique({ - where: { id: session.user.id } - }) as { chatPrivateKeyEncrypted?: string | null } | null - - if (!user?.chatPrivateKeyEncrypted) { - return NextResponse.json( - { hasBackup: false }, - { - headers: { - "Cache-Control": "no-store" - } - } - ) - } - - const decrypted = decryptString(user.chatPrivateKeyEncrypted) - if (!decrypted) { - return NextResponse.json( - { hasBackup: false }, - { - headers: { - "Cache-Control": "no-store" - } - } - ) - } - - return NextResponse.json( - { hasBackup: true, privateKey: decrypted }, - { - headers: { - "Cache-Control": "no-store" - } - } - ) - } catch (error) { - console.error("[CHAT_KEY_GET]", error) - return new NextResponse("Internal Error", { status: 500 }) - } -} - diff --git a/src/app/api/user/chat-setup/route.ts b/src/app/api/user/chat-setup/route.ts deleted file mode 100644 index dfe34ce..0000000 --- a/src/app/api/user/chat-setup/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextResponse } from "next/server" -import { auth } from "@/auth" -import { prisma } from "@/lib/prisma" -import { encryptString } from "@/lib/encryption" - -export async function POST(req: Request) { - try { - const session = await auth() - if (!session?.user?.id) { - return new NextResponse("Unauthorized", { status: 401 }) - } - - const { publicKey, privateKey } = await req.json() as { - publicKey?: string - privateKey?: string - } - - if (!publicKey) { - return new NextResponse("Missing publicKey", { status: 400 }) - } - - let chatPrivateKeyEncrypted: string | undefined - if (privateKey) { - try { - chatPrivateKeyEncrypted = encryptString(privateKey) - } catch (err) { - console.error("[CHAT_SETUP] Failed to encrypt private key", err) - return new NextResponse("Failed to encrypt private key", { status: 500 }) - } - } - - await prisma.user.update({ - where: { id: session.user.id }, - data: { - chatPublicKey: publicKey, - ...(chatPrivateKeyEncrypted ? { chatPrivateKeyEncrypted } : {}) - } - }) - - return NextResponse.json({ success: true }) - } catch (error) { - console.error("[CHAT_SETUP]", error) - return new NextResponse("Internal Error", { status: 500 }) - } -} diff --git a/src/app/supplier/orders/[orderId]/page.tsx b/src/app/supplier/orders/[orderId]/page.tsx new file mode 100644 index 0000000..b399487 --- /dev/null +++ b/src/app/supplier/orders/[orderId]/page.tsx @@ -0,0 +1,348 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import { useParams, useRouter } from "next/navigation" +import { ArrowLeft, Loader2, AlertCircle, Package, Truck, Save, CheckCircle2, User, Mail, Phone, MapPin } from "lucide-react" +import toast from "react-hot-toast" + +const SHOPIFY_TRACKING_COMPANIES = [ + "4PX", + "Amazon Logistics UK", + "An Post", + "Australia Post", + "UPS", + "USPS", + "FedEx", + "DHL Express", + "DHL eCommerce", + "Canada Post", + "China Post", + "DPD", + "Royal Mail", + "Purolator", + "OnTrac", + "GLS", + "PostNL", + "Singapore Post", + "La Poste", + "Yodel", + "Evri", + "Japan Post", + "New Zealand Post", + "Aramex", + "Other", +] + +type SupplierOrder = { + id: string + orderNumber: string + email?: string | null + totalPrice: string + currency?: string | null + financialStatus?: string | null + fulfillmentStatus: string + trackingNumber?: string | null + trackingCompany?: string | null + trackingUrl?: string | null + createdAt: string + lineItems: Array<{ id?: number; title?: string; quantity?: number; price?: string; sku?: string; variant_title?: string; product_id?: number }> + customer?: { first_name?: string; last_name?: string; email?: string; phone?: string } | null + shippingAddress?: { first_name?: string; last_name?: string; address1?: string; address2?: string; city?: string; province?: string; country?: string; zip?: string; phone?: string } | null + billingAddress?: { first_name?: string; last_name?: string; address1?: string; address2?: string; city?: string; province?: string; country?: string; zip?: string; phone?: string } | null + store: { id: string; businessName: string; shopifyStoreName: string } +} + +export default function SupplierOrderDetailsPage() { + const router = useRouter() + const params = useParams() + const orderId = params?.orderId as string + + const [order, setOrder] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [error, setError] = useState("") + + const [fulfillmentStatus, setFulfillmentStatus] = useState("UNFULFILLED") + const [trackingNumber, setTrackingNumber] = useState("") + const [trackingCompany, setTrackingCompany] = useState("") + const [trackingUrl, setTrackingUrl] = useState("") + + useEffect(() => { + if (!orderId) return + const fetchOrder = async () => { + setLoading(true) + setError("") + try { + const res = await fetch(`/api/supplier/orders/${orderId}`) + if (!res.ok) throw new Error("Failed to load order") + const data = await res.json() + const o = data.order as SupplierOrder + setOrder(o) + setFulfillmentStatus(o.fulfillmentStatus || "UNFULFILLED") + setTrackingNumber(o.trackingNumber || "") + setTrackingCompany(o.trackingCompany || "") + setTrackingUrl(o.trackingUrl || "") + } catch { + setError("Failed to load order details.") + } finally { + setLoading(false) + } + } + fetchOrder() + }, [orderId]) + + const changed = useMemo(() => { + if (!order) return false + return ( + fulfillmentStatus !== (order.fulfillmentStatus || "UNFULFILLED") || + trackingNumber !== (order.trackingNumber || "") || + trackingCompany !== (order.trackingCompany || "") || + trackingUrl !== (order.trackingUrl || "") + ) + }, [order, fulfillmentStatus, trackingNumber, trackingCompany, trackingUrl]) + + const save = async () => { + if (!order) return + setSaving(true) + try { + const res = await fetch(`/api/supplier/orders/${order.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fulfillmentStatus, + trackingNumber, + trackingCompany, + trackingUrl, + }), + }) + if (!res.ok) throw new Error("Failed to save") + toast.success("Order updated successfully") + setOrder({ + ...order, + fulfillmentStatus, + trackingNumber, + trackingCompany, + trackingUrl, + }) + } catch { + toast.error("Failed to update order") + } finally { + setSaving(false) + } + } + + if (loading) { + return ( +
+ +
+ ) + } + + if (error || !order) { + return ( +
+ +

{error || "Order not found"}

+ +
+ ) + } + + return ( +
+
+
+ +
+

Order {order.orderNumber}

+

{order.store.businessName}

+
+
+ +
+ +
+
+
+

+ + Order Snapshot +

+
+
+

Created

+

{new Date(order.createdAt).toLocaleString()}

+
+
+

Total

+

{order.totalPrice} {order.currency || ""}

+
+
+

Payment

+

{order.financialStatus || "N/A"}

+
+
+
+
+ +
+

+ + Fulfillment Update +

+ +
+ + +
+ +
+ + +
+ +
+ + setTrackingNumber(e.target.value)} + placeholder="e.g. 1Z999AA10123456784" + className="w-full h-11 rounded-xl bg-white/10 border border-white/10 text-white px-3" + /> +
+ +
+ + setTrackingUrl(e.target.value)} + placeholder="https://..." + className="w-full h-11 rounded-xl bg-white/10 border border-white/10 text-white px-3" + /> +
+ + {changed && ( +

+ Unsaved changes +

+ )} +
+
+ +
+
+

+ + Customer +

+
+

+ {[order.customer?.first_name, order.customer?.last_name].filter(Boolean).join(" ") || "—"} +

+

+ + {order.customer?.email || order.email || "Email: --"} +

+

+ + {order.customer?.phone || "Phone: --"} +

+
+
+ +
+

+ + Shipping Address +

+ {order.shippingAddress ? ( +
+

+ {[order.shippingAddress.first_name, order.shippingAddress.last_name].filter(Boolean).join(" ")} +

+

{order.shippingAddress.address1 || "--"}

+ {order.shippingAddress.address2 ?

{order.shippingAddress.address2}

: null} +

{[order.shippingAddress.city, order.shippingAddress.province, order.shippingAddress.zip].filter(Boolean).join(", ") || "--"}

+

{order.shippingAddress.country || "--"}

+
+ ) : ( +

No shipping address available.

+ )} +
+ +
+

+ + Billing Address +

+ {order.billingAddress ? ( +
+

+ {[order.billingAddress.first_name, order.billingAddress.last_name].filter(Boolean).join(" ")} +

+

{order.billingAddress.address1 || "--"}

+ {order.billingAddress.address2 ?

{order.billingAddress.address2}

: null} +

{[order.billingAddress.city, order.billingAddress.province, order.billingAddress.zip].filter(Boolean).join(", ") || "--"}

+

{order.billingAddress.country || "--"}

+
+ ) : ( +

No billing address available.

+ )} +
+
+ +
+

+ + Order Items +

+
+ {order.lineItems.length === 0 ? ( +

No items available.

+ ) : order.lineItems.map((item, idx) => ( +
+
+

{item.title || "Item"}

+ {item.variant_title ?

{item.variant_title}

: null} +

Qty: {item.quantity || 0}{item.sku ? ` · SKU: ${item.sku}` : ""}

+
+

+ {((Number(item.price || 0) || 0) * (item.quantity || 0)).toFixed(2)} {order.currency || ""} +

+
+ ))} +
+
+
+ ) +} + diff --git a/src/app/supplier/orders/page.tsx b/src/app/supplier/orders/page.tsx index 9f0cd27..8949189 100644 --- a/src/app/supplier/orders/page.tsx +++ b/src/app/supplier/orders/page.tsx @@ -1,18 +1,18 @@ "use client" import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" import { ShoppingCart, Search, Loader2, AlertCircle, Package, - CheckCircle2, - Clock, - XCircle + ChevronRight } from "lucide-react" export default function SupplierOrdersPage() { + const router = useRouter() const [orders, setOrders] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState("") @@ -106,7 +106,7 @@ export default function SupplierOrdersPage() {

) : ( -
+
@@ -116,11 +116,16 @@ export default function SupplierOrdersPage() { + {filteredOrders.map((order) => ( - + router.push(`/supplier/orders/${order.id}`)} + > + + - ))} diff --git a/src/app/uploads/[...path]/route.ts b/src/app/uploads/[...path]/route.ts new file mode 100644 index 0000000..8dfa97c --- /dev/null +++ b/src/app/uploads/[...path]/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server" +import fs from "node:fs/promises" +import path from "node:path" + +function contentTypeFor(filePath: string): string { + const ext = path.extname(filePath).toLowerCase() + switch (ext) { + case ".png": + return "image/png" + case ".jpg": + case ".jpeg": + return "image/jpeg" + case ".webp": + return "image/webp" + case ".gif": + return "image/gif" + case ".svg": + return "image/svg+xml" + case ".ico": + return "image/x-icon" + default: + return "application/octet-stream" + } +} + +export async function GET( + _req: Request, + { params }: { params: Promise<{ path: string[] }> }, +) { + try { + const { path: segments } = await params + if (!segments?.length) { + return NextResponse.json({ message: "Not found" }, { status: 404 }) + } + + const uploadsRoot = path.resolve("public", "uploads") + const requestedPath = path.resolve(uploadsRoot, ...segments) + + // Prevent path traversal outside public/uploads. + if (!requestedPath.startsWith(uploadsRoot)) { + return NextResponse.json({ message: "Not found" }, { status: 404 }) + } + + const data = await fs.readFile(requestedPath) + return new NextResponse(new Uint8Array(data), { + status: 200, + headers: { + "Content-Type": contentTypeFor(requestedPath), + "Cache-Control": "public, max-age=31536000, immutable", + }, + }) + } catch { + return NextResponse.json({ message: "Not found" }, { status: 404 }) + } +} + diff --git a/src/auth.supplier.ts b/src/auth.supplier.ts index bcbf472..a50e810 100644 --- a/src/auth.supplier.ts +++ b/src/auth.supplier.ts @@ -95,19 +95,18 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ const user = userId ? await prisma.user.findUnique({ where: { id: userId }, - select: { id: true, role: true, chatPublicKey: true }, + select: { id: true, role: true }, }) : sessionEmail ? await prisma.user.findUnique({ where: { email: sessionEmail }, - select: { id: true, role: true, chatPublicKey: true }, + select: { id: true, role: true }, }) : null if (user) { session.user.id = user.id session.user.role = user.role - session.user.chatPublicKey = user.chatPublicKey ?? undefined } return session diff --git a/src/auth.ts b/src/auth.ts index a7e8267..bc3884c 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -89,19 +89,18 @@ const nextAuth = NextAuth({ const user = userId ? await prisma.user.findUnique({ where: { id: userId }, - select: { id: true, role: true, chatPublicKey: true }, + select: { id: true, role: true }, }) : sessionEmail ? await prisma.user.findUnique({ where: { email: sessionEmail }, - select: { id: true, role: true, chatPublicKey: true }, + select: { id: true, role: true }, }) : null if (user) { session.user.id = user.id session.user.role = user.role - session.user.chatPublicKey = user.chatPublicKey ?? undefined } return session diff --git a/src/auth.vendor.ts b/src/auth.vendor.ts index 40757d1..b320b7f 100644 --- a/src/auth.vendor.ts +++ b/src/auth.vendor.ts @@ -95,19 +95,18 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ const user = userId ? await prisma.user.findUnique({ where: { id: userId }, - select: { id: true, role: true, chatPublicKey: true }, + select: { id: true, role: true }, }) : sessionEmail ? await prisma.user.findUnique({ where: { email: sessionEmail }, - select: { id: true, role: true, chatPublicKey: true }, + select: { id: true, role: true }, }) : null if (user) { session.user.id = user.id session.user.role = user.role - session.user.chatPublicKey = user.chatPublicKey ?? undefined } return session diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 0dff808..b31e144 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -2,9 +2,7 @@ import { useState, useEffect, useRef } from "react" import { usePathname } from "next/navigation" -import { getSession, useSession } from "next-auth/react" -import { useChatCrypto, PRIVATE_KEY_STORAGE_KEY, PUBLIC_KEY_STORAGE_KEY } from "@/hooks/useChatCrypto" -import { ChatCrypto } from "@/lib/chat-crypto" +import { useSession } from "next-auth/react" import { useTheme } from "@/components/ThemeProvider" import { Send, @@ -12,7 +10,6 @@ import { ShieldCheck, Loader2, Plus, - Lock, MessageSquare, CheckCheck, Package, @@ -28,7 +25,6 @@ interface Contact { name: string email: string image: string | null - publicKey: string | null role: string } @@ -71,8 +67,6 @@ export function ChatView() { const isLight = theme === "light" const { data: session } = useSession() const userId = (session?.user as { id?: string })?.id - const { hasKeys, loading: cryptoLoading, setupKeys, privateKey, getStoredPublicKey } = useChatCrypto(userId) - const [contacts, setContacts] = useState([]) const [selectedContact, setSelectedContact] = useState(null) const [messages, setMessages] = useState([]) @@ -92,57 +86,11 @@ export function ChatView() { const attachRef = useRef(null) const messagesEndRef = useRef(null) - const autoSetupAttemptedRef = useRef(false) - - const handleSetup = async () => { - try { - setContactsError(null) - // Ensure we have a user id before trying to set up keys - if (!userId) { - setContactsError("Session not ready. Please refresh the page and try again.") - return - } - await setupKeys() - await getSession() - fetchContacts() - } catch (error) { - console.error("Setup failed", error) - setContactsError("Failed to set up encryption. Please try again.") - } - } - - // Auto-setup chat keys when user has none (e.g. supplier who just accepted invitation) - useEffect(() => { - if (cryptoLoading || hasKeys || autoSetupAttemptedRef.current || !userId) return - autoSetupAttemptedRef.current = true - setContactsError(null) - setupKeys() - .then(() => getSession()) - .then(() => fetchContacts()) - .catch((err) => { - console.error("Auto setup failed", err) - autoSetupAttemptedRef.current = false - setContactsError("Encryption setup failed. Click below to try again.") - }) - }, [cryptoLoading, hasKeys, setupKeys]) const fetchContacts = async () => { try { setLoadingContacts(true) setContactsError(null) - // Sync current user's public key to DB so the other party always has our latest key (fixes decrypt when they send to us) - const storedPublic = getStoredPublicKey?.() ?? null - if (storedPublic) { - try { - await fetch("/api/user/chat-setup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ publicKey: storedPublic }) - }) - } catch { - // Non-blocking; contacts can still load - } - } const res = await fetch("/api/chat/contacts") if (res.ok) { const data = await res.json() @@ -159,7 +107,6 @@ export function ChatView() { } const fetchMessages = async (contact: Contact, silent = false) => { - if (!privateKey) return try { if (!silent) { setLoadingMessages(true) @@ -168,39 +115,14 @@ export function ChatView() { const res = await fetch(`/api/chat/messages?contactId=${contact.id}`) if (res.ok) { const data = await res.json() - const decryptedMessages = await Promise.all( - data.messages.map(async (m: { id: string; senderId: string; receiverId: string; encryptedContent: string; encryptedKeyForSender: string; encryptedKeyForReceiver: string; iv: string; createdAt: string }) => { - const primaryKey = m.senderId === contact.id ? m.encryptedKeyForReceiver : m.encryptedKeyForSender - const fallbackKey = m.senderId === contact.id ? m.encryptedKeyForSender : m.encryptedKeyForReceiver - for (const keyBlob of [primaryKey, fallbackKey]) { - try { - const decrypted = await ChatCrypto.decryptMessage( - m.encryptedContent, - keyBlob, - m.iv, - privateKey - ) - return { - id: m.id, - senderId: m.senderId, - receiverId: m.receiverId, - content: decrypted, - createdAt: m.createdAt - } - } catch { - continue - } - } - return { - id: m.id, - senderId: m.senderId, - receiverId: m.receiverId, - content: "[Encrypted Message - Error Decrypting]", - createdAt: m.createdAt - } - }) - ) - setMessages(decryptedMessages) + const plainMessages = data.messages.map((m: { id: string; senderId: string; receiverId: string; content: string; createdAt: string }) => ({ + id: m.id, + senderId: m.senderId, + receiverId: m.receiverId, + content: m.content, + createdAt: m.createdAt + })) + setMessages(plainMessages) } else if (!silent) { setMessagesError("Failed to load messages. Please try again.") } @@ -213,8 +135,8 @@ export function ChatView() { } useEffect(() => { - if (hasKeys) fetchContacts() - }, [hasKeys]) + fetchContacts() + }, []) useEffect(() => { if (selectedContact) { @@ -222,17 +144,17 @@ export function ChatView() { } else { setMessages([]) } - }, [selectedContact, privateKey]) + }, [selectedContact]) // Poll for new messages when a contact is selected const POLL_INTERVAL_MS = 8000 useEffect(() => { - if (!selectedContact || !privateKey) return + if (!selectedContact) return const interval = setInterval(() => { fetchMessages(selectedContact, true) }, POLL_INTERVAL_MS) return () => clearInterval(interval) - }, [selectedContact?.id, privateKey]) + }, [selectedContact?.id]) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) @@ -303,50 +225,17 @@ export function ChatView() { const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault() - if (!newMessage.trim() || !selectedContact || !selectedContact.publicKey || !privateKey) return + if (!newMessage.trim() || !selectedContact) return try { setSending(true) setSendError(null) - let meData: { user?: { chatPublicKey?: string } } = {} - try { - const session = await getSession() - meData = { user: session?.user ? { chatPublicKey: (session.user as { chatPublicKey?: string }).chatPublicKey } : {} } - } catch { - const res = await fetch("/api/auth/session") - meData = await res.json() - } - let myPublicKey = meData.user?.chatPublicKey - - if (!myPublicKey && getStoredPublicKey) { - const storedPublic = getStoredPublicKey() - if (storedPublic) { - const syncRes = await fetch("/api/user/chat-setup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ publicKey: storedPublic }) - }) - if (syncRes.ok) myPublicKey = storedPublic - } - } - - if (!myPublicKey) { - setSendError("Encryption key not ready. Please refresh the page and try again.") - return - } - - const encrypted = await ChatCrypto.encryptMessage( - newMessage, - myPublicKey, - selectedContact.publicKey - ) - const res = await fetch("/api/chat/messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ receiverId: selectedContact.id, - ...encrypted + content: newMessage }) }) @@ -372,44 +261,6 @@ export function ChatView() { } } - if (cryptoLoading) { - return ( -
- -

Initializing JUNO CHAT...

-
- ) - } - - if (!hasKeys) { - return ( -
-
- -
-

Enable JUNO CHAT

-

- JUNO CHAT uses industry-standard AES-256 E2E Encryption. - Set up your secure keys to start chatting with your connections. -

- -

- Private keys never leave your browser -

-
- ) - } - const filteredContacts = contacts.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()) || c.email.toLowerCase().includes(searchQuery.toLowerCase()) @@ -512,10 +363,7 @@ export function ChatView() { {contact.role}

-
+
))}
@@ -533,7 +381,7 @@ export function ChatView() {

YOU

- Encrypted Ready + Ready

@@ -575,7 +423,7 @@ export function ChatView() { Active Now
- End-to-End + Chat
@@ -597,8 +445,8 @@ export function ChatView() { ) : messages.length === 0 ? (
-

Secure Connection Started

-

Messages are encrypted before leaving your device.

+

Conversation Started

+

Start chatting with your connection.

) : ( messages.map((msg, idx) => { @@ -647,52 +495,19 @@ export function ChatView() {
{sendError}
- {sendError.includes("Encryption key not ready") && ( - - )}
)} - {!selectedContact.publicKey ? ( -
-

- Connection Not Ready -

-

- This contact hasn't set up their JUNO CHAT keys yet. -

-

- Ask them to open JUNO CHAT once; keys are set up automatically. -

-
- ) : ( -
+
Date Status TotalAction
{order.orderNumber} {order.vendorName} @@ -128,14 +133,20 @@ export default function SupplierOrdersPage() { - {order.status} + {order.status || "UNFULFILLED"} + + ${Number(order.total || 0).toFixed(2)} + + Manage ${order.total?.toFixed(2)}