From a06bf4dc5e96ed103ca433ce404024e4797344da Mon Sep 17 00:00:00 2001 From: Darshan Odedara Date: Mon, 4 Aug 2025 10:08:39 +0530 Subject: [PATCH 01/15] Update user data handling in sidebar and site header; implement sign-out functionality in Dashboard. --- app/contact/page.tsx | 1 - app/login/page.tsx | 1 - app/register/page.tsx | 1 - components/sidebar/app-sidebar.tsx | 26 +++++++++++++++++++------- components/sidebar/site-header.tsx | 8 +++++++- lib/auth.ts | 7 ++++++- 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 41a53fd..19acad9 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -20,7 +20,6 @@ export default function ContactPage() {
Image diff --git a/app/login/page.tsx b/app/login/page.tsx index b166c9b..750bccd 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -33,7 +33,6 @@ export default function LoginPage() {
Image diff --git a/app/register/page.tsx b/app/register/page.tsx index 5d61433..373202c 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -32,7 +32,6 @@ export default function RegisterPage() {
Image diff --git a/components/sidebar/app-sidebar.tsx b/components/sidebar/app-sidebar.tsx index c8600c0..6ad7f62 100644 --- a/components/sidebar/app-sidebar.tsx +++ b/components/sidebar/app-sidebar.tsx @@ -1,6 +1,6 @@ "use client" -import * as React from "react" +import { useState, useEffect } from "react" import { LayoutDashboard, FileVolume, @@ -33,13 +33,9 @@ import { } from "@/components/ui/sidebar" import Link from "next/link" import Logo from "../Logo" +import { useSession } from "next-auth/react" const data = { - user: { - name: "Darshan Odedara", - gstin: "22AAHFO1234F1Z5", - avatar: "/avatars/shadcn.jpg", - }, navMain: [ { title: "Dashboard", @@ -119,6 +115,22 @@ const data = { } export function AppSidebar({ ...props }: React.ComponentProps) { + const [user, setUser] = useState({ + name: "", + gstin: "", + avatar: "", + }); + const { data: session } = useSession(); + useEffect(() => { + if (session?.user) { + setUser({ + name: session.user.business_name || "", + gstin: session.user.gstin || "", + avatar: session.user.profile_url || "", + }) + } + console.log("Session data:", session); + }, []); return ( ) { - + ) diff --git a/components/sidebar/site-header.tsx b/components/sidebar/site-header.tsx index 820c9b2..694caaa 100644 --- a/components/sidebar/site-header.tsx +++ b/components/sidebar/site-header.tsx @@ -15,6 +15,8 @@ import { Separator } from "@/components/ui/separator" import { useSidebar } from "@/components/ui/sidebar" import { usePathname } from "next/navigation" import { ModeToggle } from "@/components/ModeToggle" +import { signOut } from "next-auth/react" + export function SiteHeader() { const { toggleSidebar } = useSidebar() // Get the current pathname from Next.js router @@ -63,7 +65,11 @@ export function SiteHeader() {
- diff --git a/lib/auth.ts b/lib/auth.ts index dee06e0..b40841c 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -46,13 +46,14 @@ export const authOptions: NextAuthOptions = { } // If password is valid, return user data - return { + const user = { id: userData.user_id, // Required by NextAuth gstin: userData.gstin, user_id: userData.user_id, business_name: userData.business_name, profile_url: userData.profile_url || null, }; + return user as NextAuthUser; }catch (error) { console.error("Error during authorization:", error); @@ -72,6 +73,8 @@ export const authOptions: NextAuthOptions = { token.user_id = user.user_id as string; token.business_name = user.business_name as string; token.profile_url = user.profile_url as string; + + // console.log("JWT token created:", token); } return token; }, @@ -85,6 +88,8 @@ export const authOptions: NextAuthOptions = { session.user.user_id = token.user_id as string; session.user.business_name = token.business_name as string; session.user.profile_url = token.profile_url as string | undefined; + + // console.log("Session created:", session.user); } return session; }, From 6e4d1429d903662086e90d0d5f3329d55f2048c5 Mon Sep 17 00:00:00 2001 From: Darshan Odedara Date: Mon, 4 Aug 2025 23:51:12 +0530 Subject: [PATCH 02/15] Create Profile card --- app/dashboard/profile/page.tsx | 8 ++-- app/page.tsx | 4 +- components/Profile.tsx | 57 ++++++++++++++++++++++++++ components/sidebar/app-sidebar.tsx | 25 +++++------- mw.ts | 62 +++++++++++++++++------------ public/avatar.jpg | Bin 0 -> 27041 bytes 6 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 components/Profile.tsx create mode 100644 public/avatar.jpg diff --git a/app/dashboard/profile/page.tsx b/app/dashboard/profile/page.tsx index 76bedc0..6dc35cd 100644 --- a/app/dashboard/profile/page.tsx +++ b/app/dashboard/profile/page.tsx @@ -1,8 +1,10 @@ +import ProfileCard from "@/components/Profile"; + export default function Home() { return ( -
-
-

Profile page

+
+
+
); diff --git a/app/page.tsx b/app/page.tsx index b3ab0f4..81d9240 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,7 +8,7 @@ import Footer from "@/components/Footer" import { Header } from "@/components/Header" import { useState } from "react"; import { Subscribe } from "./actions"; - +import Link from "next/link"; export default function LandingPage() { const [email, setEmail] = useState(""); return ( @@ -29,7 +29,7 @@ export default function LandingPage() {

diff --git a/components/Profile.tsx b/components/Profile.tsx new file mode 100644 index 0000000..d5ee2ca --- /dev/null +++ b/components/Profile.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +// import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { useMemo } from "react"; +import { useSession } from "next-auth/react"; + +export default function ProfileCard() { + const { data: session } = useSession(); + + const user = useMemo(() => ({ + name: session?.user?.business_name || "", + gstin: session?.user?.gstin || "", + avatar: session?.user?.profile_url || "", + }), [session?.user]); + return ( + + + {/*

Your Business Profile

*/} +
+
+
+ +
+
+ +
+
+ + + DS + +
+ +
+
+ +

Domestic courier service with over 2000+ service locations. Nation-wide fast and reliable courier service.

+
+
+ +

Jayraj Vasahat, Near Canara Bank, Hazira, Surat, Gujarat, 394510

+
+
+ +
    +
  • darshan.odedara@gmail.com
  • +
  • 6351382297
  • +
+
+
+
+
+ ); +} diff --git a/components/sidebar/app-sidebar.tsx b/components/sidebar/app-sidebar.tsx index 6ad7f62..eb2d655 100644 --- a/components/sidebar/app-sidebar.tsx +++ b/components/sidebar/app-sidebar.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useMemo } from "react"; import { LayoutDashboard, FileVolume, @@ -115,22 +115,15 @@ const data = { } export function AppSidebar({ ...props }: React.ComponentProps) { - const [user, setUser] = useState({ - name: "", - gstin: "", - avatar: "", - }); const { data: session } = useSession(); - useEffect(() => { - if (session?.user) { - setUser({ - name: session.user.business_name || "", - gstin: session.user.gstin || "", - avatar: session.user.profile_url || "", - }) - } - console.log("Session data:", session); - }, []); + + const user = useMemo(() => ({ + name: session?.user?.business_name || "", + gstin: session?.user?.gstin || "", + avatar: session?.user?.profile_url || "", + }), [session?.user]); + + // console.log("Session data:", session); return ( pathname.startsWith(route))) { - return NextResponse.redirect(new URL("/login", request.url)); + // Protect dashboard routes + if (pathname.startsWith('/dashboard')) { + if (!token) { + return NextResponse.redirect(new URL('/login', request.url)); + } } - // If user is signed in and tries to access login/signup - if (token && authPages.some((route) => pathname.startsWith(route))) { - return NextResponse.redirect(new URL("/dashboard", request.url)); + // Redirect authenticated users away from login/register pages + if ((pathname === '/login' || pathname === '/register') && token) { + return NextResponse.redirect(new URL('/dashboard', request.url)); } return NextResponse.next(); } -// export default async function withAuth( -// middleware, -// { -// callbacks: { -// authorized: async ({ token, request }) => { -// // Allow access if the user is authenticated -// return !!token; -// }, -// } -// } -// ) -// export const config = { - matcher: ["/dashboard/:path*", "/profile/:path*", "/login", "/register"], + matcher: [ + "/dashboard/:path*", + "/profile/:path*", + "/login", + "/register", + "/api/:path*" // Add API routes to matcher + ], }; diff --git a/public/avatar.jpg b/public/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cc2871f2bdb122186e85fbd2f9ddf9d8ff8ef43e GIT binary patch literal 27041 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 literal 0 HcmV?d00001 From 683ed940f03233c4a20d62a9b83bc6d594909805 Mon Sep 17 00:00:00 2001 From: Darshan Odedara Date: Wed, 6 Aug 2025 23:34:51 +0530 Subject: [PATCH 03/15] Implement invoice creation API and associated schemas; enhance invoice form handling in dashboard. Unaithorized (token) error. --- app/api/invoice/create/route.ts | 116 ++++++++++++++++++++++++ app/dashboard/invoices/page.tsx | 150 +++++++++++++++++++++++--------- db/types/history.ts | 18 ++++ db/types/invoice.ts | 17 ++++ db/types/transaction.ts | 0 db/types/user.ts | 0 6 files changed, 259 insertions(+), 42 deletions(-) create mode 100644 app/api/invoice/create/route.ts create mode 100644 db/types/history.ts create mode 100644 db/types/invoice.ts create mode 100644 db/types/transaction.ts create mode 100644 db/types/user.ts diff --git a/app/api/invoice/create/route.ts b/app/api/invoice/create/route.ts new file mode 100644 index 0000000..8cfa7a9 --- /dev/null +++ b/app/api/invoice/create/route.ts @@ -0,0 +1,116 @@ +import { supabaseAdmin } from "@/db/connect"; +import { NextResponse, NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; +import { InvoiceSchema } from "@/db/types/invoice"; + +export async function POST(req: NextRequest) { + try { + // Check authentication using JWT token + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET + }); + + console.log('Token:', token); + + if (!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const formData = await req.formData(); + + // Extract form fields + const senderGstin = formData.get("senderGstin") as string; + const receiverGstin = formData.get("receiverGstin") as string; + const amount = formData.get("amount") as string; + const title = formData.get("title") as string; + const invoiceNumber = formData.get("invoiceNumber") as string; + const description = formData.get("description") as string; + const invoiceDate = formData.get("invoiceDate") as string; + const file = formData.get("invoiceFile") as File; + + // Validate that the sender GST matches the authenticated user + if (senderGstin !== token.gstin) { + return NextResponse.json({ error: "Sender GST number doesn't match authenticated user" }, { status: 403 }); + } + + // Validate required fields + if (!senderGstin || !receiverGstin || !amount || !title || !invoiceNumber || !invoiceDate) { + return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); + } + + // Generate unique invoice ID + const invoiceId = crypto.randomUUID(); + + // Get sender user ID from token (already authenticated) + const senderUserId = token.user_id as string; + + // Get receiver user ID from GST number + const { data: receiverUser } = await supabaseAdmin + .from("users") + .select("user_id") + .eq("gstin", receiverGstin) + .single(); + + if (!receiverUser) { + return NextResponse.json({ error: "Invalid receiver GST number" }, { status: 400 }); + } + + // Prepare invoice data + const invoiceData = { + invoice_id: invoiceId, + sender_gstin: senderGstin, + recipient_gstin: receiverGstin, + amount: parseFloat(amount), + status: "requested", + invoice_date: invoiceDate, + sender_id: senderUserId, + recipient_id: receiverUser.user_id, + title: title, + description: description || null, + invoice_number: invoiceNumber, + }; + + // Validate data with Zod schema + const validatedData = InvoiceSchema.parse(invoiceData); + + // Upload file to Supabase Storage if provided + let filePath = null; + if (file && file.size > 0) { + const fileExtension = file.name.split('.').pop(); + filePath = `${senderGstin}/${invoiceId}.${fileExtension}`; + + const { error: uploadError } = await supabaseAdmin.storage + .from("invoices") + .upload(filePath, file); + + if (uploadError) { + console.error("File upload error:", uploadError); + return NextResponse.json({ error: "File upload failed" }, { status: 500 }); + } + } + + // Create invoice entry in database + const { data: newInvoice, error: dbError } = await supabaseAdmin + .from("invoices") + .insert([validatedData]) + .select() + .single(); + + if (dbError) { + console.error("Database error:", dbError); + return NextResponse.json({ error: "Failed to create invoice" }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + invoice: newInvoice + }, { status: 201 }); + + } catch (error) { + console.error("Invoice creation error:", error); + return NextResponse.json({ + error: "Internal server error" + }, { status: 500 }); + } +} diff --git a/app/dashboard/invoices/page.tsx b/app/dashboard/invoices/page.tsx index 00d99cc..a8a4ff8 100644 --- a/app/dashboard/invoices/page.tsx +++ b/app/dashboard/invoices/page.tsx @@ -5,27 +5,18 @@ import { ChevronDownIcon } from "lucide-react" import { Calendar } from "@/components/ui/calendar" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" +import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" +import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; +import { useSession } from "next-auth/react"; export default function Page() { + const { data: session } = useSession(); + const [open, setOpen] = useState(false) - const [date, setDate] = useState(undefined) - const senderGstNumber = "27AAACF1234A1Z5" // Example GST number + const [date, setDate] = useState(undefined); + const [isSubmitting, setIsSubmitting] = useState(false); const [invoice, setInvoice] = useState(null) const [receiverGstNumber, setReceiverGstNumber] = useState("") @@ -33,7 +24,73 @@ export default function Page() { const [title, setTitle] = useState("") const [invoiceNumber, setInvoiceNumber] = useState("") const [description, setDescription] = useState("") + const senderGstNumber = session?.user?.gstin || "" // Assuming gstNumber is stored in user object // const [verified, setVerified] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Validation + if (!receiverGstNumber || !amount || !title || !invoiceNumber || !date) { + alert("Please fill in all required fields"); + return; + } + if (!senderGstNumber) { + alert("Sender GST number not found. Please log in again."); + return; + } + if (receiverGstNumber === senderGstNumber) { + alert("Sender and receiver GST numbers cannot be the same"); + return; + } + + setIsSubmitting(true); + + try { + // Prepare form data + const formData = new FormData(); + formData.append("senderGstin", senderGstNumber); + formData.append("receiverGstin", receiverGstNumber); + formData.append("amount", amount); + formData.append("title", title); + formData.append("invoiceNumber", invoiceNumber); + formData.append("description", description); + formData.append("invoiceDate", date.toISOString()); + + if (invoice) { + formData.append("invoiceFile", invoice); + } + + // Submit to API + const response = await fetch("/api/invoice/create", { + method: "POST", + body: formData, + }); + + const result = await response.json(); + + if (response.ok) { + alert("Invoice created successfully!"); + // Reset form + setReceiverGstNumber(""); + setAmount(""); + setTitle(""); + setInvoiceNumber(""); + setDescription(""); + setDate(undefined); + setInvoice(null); + // Optionally redirect to invoices list + // window.location.href = "/dashboard/invoices/sent"; + } else { + alert(`Error: ${result.error || "Failed to create invoice"}`); + } + } catch (error) { + console.error("Error creating invoice:", error); + alert("An error occurred while creating the invoice"); + } finally { + setIsSubmitting(false); + } + }; return (
@@ -49,24 +106,24 @@ export default function Page() { {/* Form will be hidden until GST number is verified */} {/*
*/} - +
- +
- setReceiverGstNumber(e.target.value)} @@ -83,15 +140,17 @@ export default function Page() {
- - { - if (e.target.files && e.target.files[0]) { - setInvoice(e.target.files[0]) - } - }} - id="invoice" type="file" required/> + + { + if (e.target.files && e.target.files[0]) { + setInvoice(e.target.files[0]) + } + }} + id="invoice" + type="file" + accept=".pdf,.png,.jpg,.jpeg" + />
- - Amount (INR) * + setInvoiceNumber(e.target.value)} @@ -159,8 +220,8 @@ export default function Page() {
-