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 0000000..5c52822
Binary files /dev/null and b/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/3204a4a4-4580-44fa-8320-c0d3f2d93125.png differ
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 0000000..dd5a126
Binary files /dev/null and b/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/40791d23-ead0-4ab9-b104-5f0a1b30d2b3.ico differ
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 0000000..b767110
Binary files /dev/null and b/public/uploads/supplier-products/cmna11jb0000d4lpe2hklimdf/f266e026-e6c5-458e-87f0-1727a7c55ba1.png differ
diff --git a/src/app/api/chat/contacts/route.ts b/src/app/api/chat/contacts/route.ts
index 80e0ec5..d34ac02 100644
--- a/src/app/api/chat/contacts/route.ts
+++ b/src/app/api/chat/contacts/route.ts
@@ -25,7 +25,6 @@ export async function GET() {
name: string | null
email: string | null
image: string | null
- publicKey: string | null
role: string
}
let contacts: Contact[] = []
@@ -46,7 +45,6 @@ export async function GET() {
name: true,
email: true,
image: true,
- chatPublicKey: true,
}
}
}
@@ -59,7 +57,6 @@ export async function GET() {
name: c.supplier.companyName || c.supplier.user.name,
email: c.supplier.user.email,
image: c.supplier.user.image,
- publicKey: c.supplier.user.chatPublicKey,
role: "SUPPLIER"
}))
} else if (user.role === "SUPPLIER" && user.supplierProfile) {
@@ -78,7 +75,6 @@ export async function GET() {
name: true,
email: true,
image: true,
- chatPublicKey: true,
}
}
}
@@ -91,7 +87,6 @@ export async function GET() {
name: c.store.businessName || c.store.user.name,
email: c.store.user.email,
image: c.store.user.image,
- publicKey: c.store.user.chatPublicKey,
role: "VENDOR"
}))
}
diff --git a/src/app/api/chat/messages/route.ts b/src/app/api/chat/messages/route.ts
index c37ced0..1a5d4ea 100644
--- a/src/app/api/chat/messages/route.ts
+++ b/src/app/api/chat/messages/route.ts
@@ -10,9 +10,9 @@ export async function POST(req: Request) {
return new NextResponse("Unauthorized", { status: 401 })
}
- const { receiverId, encryptedContent, encryptedKeyForSender, encryptedKeyForReceiver, iv } = await req.json()
+ const { receiverId, content } = await req.json()
- if (!receiverId || !encryptedContent || !encryptedKeyForSender || !encryptedKeyForReceiver || !iv) {
+ if (!receiverId || !content || typeof content !== "string") {
return new NextResponse("Missing fields", { status: 400 })
}
@@ -20,18 +20,15 @@ export async function POST(req: Request) {
data: {
senderId: session.user.id,
receiverId,
- encryptedContent,
- encryptedKeyForSender,
- encryptedKeyForReceiver,
- iv,
+ content,
}
})
// Send push notification to receiver
try {
await sendPushNotification(receiverId, {
- title: "New Encrypted Message",
- body: `${session.user.name || "A user"} sent you a secure message.`,
+ title: "New Message",
+ body: `${session.user.name || "A user"} sent you a message.`,
url: "/dashboard/chat" // Correct to your chat dashboard path
})
} catch (pushError) {
diff --git a/src/app/api/supplier/orders/[orderId]/route.ts b/src/app/api/supplier/orders/[orderId]/route.ts
new file mode 100644
index 0000000..a1ba7ca
--- /dev/null
+++ b/src/app/api/supplier/orders/[orderId]/route.ts
@@ -0,0 +1,216 @@
+import { NextResponse } from "next/server"
+import { auth } from "@/auth"
+import { prisma } from "@/lib/prisma"
+
+async function getSupplierContext(userId: string) {
+ const profile = await prisma.supplierProfile.findUnique({
+ where: { userId },
+ select: { id: true },
+ })
+ if (!profile) return null
+
+ const acceptedSyncs = await prisma.productSync.findMany({
+ where: { supplierId: profile.id, status: "ACCEPTED" },
+ select: { storeId: true, shopifyProductId: true },
+ })
+
+ const syncsByStore = new Map>()
+ 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() {
| Date |
Status |
Total |
+ Action |
{filteredOrders.map((order) => (
-
+
router.push(`/supplier/orders/${order.id}`)}
+ >
| {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)} |
))}
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.
-
-
- ) : (
-