diff --git a/.gitignore b/.gitignore index 6d764eb..5f969c2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ # testing /coverage - +.vscode # next.js /.next/ /out/ @@ -35,10 +35,13 @@ yarn-error.log* # vercel .vercel - +.swc # typescript *.tsbuildinfo next-env.d.ts *.xlsx -*.csv \ No newline at end of file +*.csv + +jest.config.ts +*.test.tsx \ No newline at end of file diff --git a/ALERT_SYSTEM.md b/ALERT_SYSTEM.md new file mode 100644 index 0000000..3c566e0 --- /dev/null +++ b/ALERT_SYSTEM.md @@ -0,0 +1,192 @@ +# Alert System Documentation + +This documentation explains how to use the reusable alert system in your components. + +## Overview + +The alert system consists of: +- `AlertProvider` - Context provider that manages alert state +- `useAlert()` - Hook to access alert context +- `useAlertActions()` - Convenient hook with pre-defined alert functions + +## Setup + +The `AlertProvider` is already set up in the dashboard layout, so any component within the dashboard can use alerts. + +## Usage in Components + +### Method 1: Using useAlertActions (Recommended) + +```typescript +import { useAlertActions } from "@/lib/use-alert"; + +export default function MyComponent() { + const { showError, showSuccess, showWarning, showInfo } = useAlertActions(); + + const handleSubmit = async () => { + try { + // Your logic here + const response = await fetch('/api/some-endpoint'); + + if (response.ok) { + showSuccess("Operation completed successfully!"); + } else { + showError("Something went wrong!"); + } + } catch (error) { + showError("Network error occurred"); + } + }; + + return ( + // Your component JSX + ); +} +``` + +### Method 2: Using useAlert directly + +```typescript +import { useAlert } from "@/lib/alert-context"; + +export default function MyComponent() { + const { showAlert } = useAlert(); + + const handleAction = () => { + showAlert("Custom message", "warning"); + }; + + return ( + // Your component JSX + ); +} +``` + +## Available Alert Types + +### 1. Error Alerts +```typescript +showError("This is an error message"); +// or +showAlert("This is an error message", "destructive"); +``` + +### 2. Success Alerts +```typescript +showSuccess("Operation completed successfully!"); +// or +showAlert("Operation completed successfully!", "success"); +``` + +### 3. Warning Alerts +```typescript +showWarning("Please check your input"); +// or +showAlert("Please check your input", "warning"); +``` + +### 4. Info Alerts +```typescript +showInfo("Here's some information"); +// or +showAlert("Here's some information", "default"); +``` + +## Alert Features + +- **Auto-dismiss**: All alerts automatically disappear after 3 seconds +- **Fixed positioning**: Alerts appear at the top center of the screen +- **Responsive design**: Alerts adapt to different screen sizes +- **Custom styling**: Each alert type has its own color scheme and icon +- **Z-index management**: Alerts appear above all other content + +## Alert Variants + +| Variant | Description | Color Scheme | Icon | +|---------|-------------|--------------|------| +| `destructive` | Error messages | Red | AlertCircle | +| `success` | Success messages | Green | CheckCircle | +| `warning` | Warning messages | Yellow | AlertTriangle | +| `default` | Info messages | Blue | Info | + +## Examples + +### Form Validation +```typescript +const { showError, showSuccess } = useAlertActions(); + +const validateForm = () => { + if (!email) { + showError("Email is required"); + return false; + } + if (!password) { + showError("Password is required"); + return false; + } + return true; +}; + +const handleSubmit = async () => { + if (!validateForm()) return; + + try { + await submitForm(); + showSuccess("Form submitted successfully!"); + } catch (error) { + showError("Failed to submit form"); + } +}; +``` + +### API Operations +```typescript +const { showError, showSuccess, showWarning } = useAlertActions(); + +const deleteItem = async (id: string) => { + try { + const response = await fetch(\`/api/items/\${id}\`, { + method: 'DELETE' + }); + + if (response.ok) { + showSuccess("Item deleted successfully!"); + } else if (response.status === 404) { + showWarning("Item not found"); + } else { + showError("Failed to delete item"); + } + } catch (error) { + showError("Network error occurred"); + } +}; +``` + +### User Feedback +```typescript +const { showInfo, showSuccess } = useAlertActions(); + +const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + showSuccess("Copied to clipboard!"); +}; + +const showHelpInfo = () => { + showInfo("Click the button to copy the GST number"); +}; +``` + +## Best Practices + +1. **Use appropriate variants**: Choose the right alert type for your message +2. **Keep messages concise**: Short, clear messages work best +3. **Don't overuse**: Avoid showing multiple alerts in quick succession +4. **Provide context**: Include relevant information in error messages +5. **Test auto-dismiss**: Ensure 3 seconds is enough time to read the message + +## Implementation Details + +- Alerts are managed by React Context +- State is automatically cleaned up when alerts hide +- The system prevents multiple alerts from stacking +- Custom CSS classes provide consistent styling across variants diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 2ab606b..7bdab68 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -4,7 +4,7 @@ import bcrypt from "bcryptjs"; export async function POST(request: NextRequest) { try { - const { gstin, user_id, password } = await request.json(); + const { gstin, user_id, password, keyAttributes } = await request.json(); if (!gstin || !password) { return NextResponse.json( @@ -13,15 +13,29 @@ export async function POST(request: NextRequest) { ); } - // Hash the password on the server side - const hashedPassword = await bcrypt.hash(password, 10); + if (!user_id) { + return NextResponse.json( + { error: "User ID is required" }, + { status: 400 } + ); + } + if (!keyAttributes) { + return NextResponse.json( + { error: "Key attributes not generated, please try again later." }, + { status: 400 } + ); + } + // Generate salt and hash the password + const salt = await bcrypt.genSalt(10); + // Hash the password + const hashedPassword = await bcrypt.hash(password, salt); // Check if the user already exists const { data: existingUser, error: userError } = await supabaseAdmin - .from("users") - .select("*") - .eq("gstin", gstin) - .single(); + .from("users") + .select("*") + .eq("gstin", gstin) + .single(); if (userError && userError.code !== "PGRST116") { return NextResponse.json( { error: "Failed to check existing user" }, @@ -34,33 +48,44 @@ export async function POST(request: NextRequest) { { status: 400 } ); } - const {data: business, error: businessError} = await supabaseAdmin - .from("businesses") - .select("*") - .eq("gstin", gstin); + const { data: business, error: businessError } = await supabaseAdmin + .from("businesses") + .select("*") + .eq("gstin", gstin); - if (businessError) { + if (!business || businessError) { console.error("Error fetching business:", businessError); return NextResponse.json( - { error: "Failed to fetch business. The GSTIN could be incorrect. Check the details." }, + { + error: + "Failed to fetch business. The GSTIN could be incorrect or the service might be temporarily unavailable.", + }, { status: 404 } ); } - const {data : newUser, error: newUserError} = await supabaseAdmin - .from("users") - .insert([{ - user_id, - gstin, - business_name: business?.[0]?.business_name, - mobile_number: business?.[0]?.mobile_number, - profile_url : null, - password: hashedPassword, // Store the server-side hashed password - }]); + // Use a transaction to ensure atomicity + const { data, error } = await supabaseAdmin.rpc('register_user_with_keys', { + p_user_id: user_id, + p_gstin: gstin, + p_business_name: business?.[0]?.business_name, + p_mobile_number: business?.[0]?.mobile_number, + p_password: hashedPassword, + p_business_address: business?.[0]?.business_address, + p_business_description: business?.[0]?.business_description, + p_encrypted_key: keyAttributes.encryptedKey, + p_key_decryption_nonce: keyAttributes.keyDecryptionNonce, + p_kek_salt: keyAttributes.kekSalt, + p_ops_limit: keyAttributes.opsLimit, + p_mem_limit: keyAttributes.memLimit, + p_public_key: keyAttributes.publicKey, + p_encrypted_secret_key: keyAttributes.encryptedSecretKey, + p_secret_key_decryption_nonce: keyAttributes.secretKeyDecryptionNonce + }); - if (newUserError) { - console.error("Error inserting user:", newUserError); + if (error) { + console.error("Error in atomic registration:", error); return NextResponse.json( - { error: "Failed to register user" }, + { error: "Failed to register user and key attributes" }, { status: 400 } ); } diff --git a/app/api/auth/verify/route.ts b/app/api/auth/verify/route.ts new file mode 100644 index 0000000..133adf1 --- /dev/null +++ b/app/api/auth/verify/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabaseAdmin } from "@/db/connect"; + +export async function POST(request: NextRequest) { + try { + const { gstin } = await request.json(); + if (!gstin) { + return NextResponse.json({ error: "GSTIN is required" }, { status: 400 }); + } + + // Fetch user by GSTIN + const { data: keyAttributes, error } = await supabaseAdmin + .from("key-attributes") + .select("*") + .eq("gstin", gstin) + .single(); + if (!keyAttributes) { + return NextResponse.json( + { error: "User not found. Register first to use InvoSafe" }, + { status: 404 } + ); + } + if (error) { + console.error("Error fetching key attributes:", error); + return NextResponse.json( + { error: "Error while fetching user details. Try later." }, + { status: 404 } + ); + } + // Only send necessary user details + const userResponse = { + encryptedKey: keyAttributes.encrypted_key, + keyDecryptionNonce: keyAttributes.key_decryption_nonce, + kekSalt: keyAttributes.kek_salt, + opsLimit: keyAttributes.ops_limit, + memLimit: keyAttributes.mem_limit, + publicKey: keyAttributes.public_key, + encryptedSecretKey: keyAttributes.encrypted_secret_key, + secretKeyDecryptionNonce: keyAttributes.secret_key_decryption_nonce, + }; + + return NextResponse.json({ keyAttributes: userResponse }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "An error occurred while processing your request." }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/invoice/active/route.ts b/app/api/invoice/active/route.ts new file mode 100644 index 0000000..19b589d --- /dev/null +++ b/app/api/invoice/active/route.ts @@ -0,0 +1,34 @@ +import { supabaseAdmin } from "@/db/connect"; +import { NextResponse, NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; + +export async function GET(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if(!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Fetching invoice requests + const {data: invoices, error: dbError} = await supabaseAdmin + .from("invoices") + .select("*") + .eq("recipient_gstin", token.gstin) + .eq("status", "active"); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + if (!invoices || invoices.length === 0) { + return NextResponse.json({ message: "No pending invoices found" }, { status: 404 }); + } + + return NextResponse.json(invoices, { status: 200 }); + }catch (error) { + return NextResponse.json({ error: "Error :" + error}, { status: 401 }); + } +} \ No newline at end of file diff --git a/app/api/invoice/create/route.ts b/app/api/invoice/create/route.ts new file mode 100644 index 0000000..e4d72c8 --- /dev/null +++ b/app/api/invoice/create/route.ts @@ -0,0 +1,109 @@ +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, + }); + + if (!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const formData = await req.formData(); + + // Extract form fields (no validation here - handled on client) + 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; + const senderUserName = formData.get("senderUserName") as string; + const primaryKey = formData.get("primaryInvoiceKey") as string; + const secondaryKey = formData.get("secondaryInvoiceKey") as string; + const decryptionHeader = formData.get("decryptionHeader") as string; + + // Generate unique invoice ID + const invoiceId = crypto.randomUUID(); + + // Get receiver user ID and business name from GST number + const { data: receiverUser } = await supabaseAdmin + .from("users") + .select("business_name") + .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_name: senderUserName, + recipient_name: receiverUser.business_name, + title: title, + description: description || undefined, + invoice_number: invoiceNumber, + primary_invoice_key: primaryKey || undefined, + secondary_invoice_key: secondaryKey || undefined, + decryption_header: decryptionHeader || undefined, + // file_path: file ? "https://fxlianxlwekzkiqarlev.supabase.co/storage/v1/object/public/invoices/" + senderGstin + "/" + invoiceId + `.${file.name.split('.').pop()}` : null, + }; + + // Validate data with Zod schema + const validatedData = InvoiceSchema.parse(invoiceData); + if( !validatedData) { + return NextResponse.json({ error: "Invalid invoice data" }, { status: 400 }); + } + // 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 , error: dbError } = await supabaseAdmin + .from("invoices") + .insert([validatedData]); + + if (dbError) { + console.error("Database error:", dbError); + return NextResponse.json({ error: "Failed to create invoice" }, { status: 500 }); + } + + return NextResponse.json({ + message: "Invoice created successfully", + success: true, + }, { status: 201 }); + + } catch (error) { + console.error("Invoice creation error:", error); + return NextResponse.json({ + error: "Internal server error" + }, { status: 500 }); + } +} diff --git a/app/api/invoice/history/route.ts b/app/api/invoice/history/route.ts new file mode 100644 index 0000000..a119b8f --- /dev/null +++ b/app/api/invoice/history/route.ts @@ -0,0 +1,34 @@ +import { supabaseAdmin } from "@/db/connect"; +import { NextResponse, NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; + +export async function GET(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if(!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Fetching invoice requests + const {data: invoices, error: dbError} = await supabaseAdmin + .from("invoices") + .select("*") + .eq("recipient_gstin", token.gstin) + .eq("status", "requested"); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + if (!invoices || invoices.length === 0) { + return NextResponse.json({ message: "No invoice requests found" }, { status: 404 }); + } + + return NextResponse.json(invoices, { status: 200 }); + }catch (error) { + return NextResponse.json({ error: "Error :" + error}, { status: 401 }); + } +} diff --git a/app/api/invoice/request/route.ts b/app/api/invoice/request/route.ts new file mode 100644 index 0000000..6ac3e05 --- /dev/null +++ b/app/api/invoice/request/route.ts @@ -0,0 +1,69 @@ +import { supabaseAdmin } from "@/db/connect"; +import { NextResponse, NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; + +export async function GET(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if(!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Fetching invoice requests + const {data: invoices, error: dbError} = await supabaseAdmin + .from("invoices") + .select("*") + .eq("recipient_gstin", token.gstin) + .eq("status", "requested"); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + if (!invoices || invoices.length === 0) { + return NextResponse.json({ message: "No invoice requests found" }, { status: 404 }); + } + + return NextResponse.json(invoices, { status: 200 }); + }catch (error) { + return NextResponse.json({ error: "Error :" + error}, { status: 401 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const invoiceId = searchParams.get("id"); + + if (!invoiceId) { + return NextResponse.json({ error: "Invoice ID is required" }, { status: 400 }); + } + + // Deleting the invoice request + const { error: dbError } = await supabaseAdmin + .from("invoices") + .delete() + .eq("id", invoiceId) + .eq("recipient_gstin", token.gstin); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + + return NextResponse.json({ message: "Invoice request deleted successfully" }, { status: 200 }); + } catch (error) { + return NextResponse.json({ error: "Error :" + error }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/invoice/route.ts b/app/api/invoice/route.ts index f8ed899..772c233 100644 --- a/app/api/invoice/route.ts +++ b/app/api/invoice/route.ts @@ -1,6 +1,36 @@ import { supabaseAdmin } from "@/db/connect"; +import { getToken } from "next-auth/jwt"; import { NextRequest, NextResponse } from "next/server"; -export async function GET(request: NextRequest) { - -} +export async function POST(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if(!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { invoice_id, gstin} = await req.json(); + if(!invoice_id || !gstin) { + return NextResponse.json({ error: "Missing invoice_id or gstin" }, { status: 400 }); + } + // Fetching invoice where sender_gstin or receiver_gstin matches the gstin + const { data: invoice, error: dbError } = await supabaseAdmin.rpc('get_invoice_details',{ + p_invoice_id : invoice_id, + p_gstin: gstin + }); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + if (!invoice) { + return NextResponse.json({ message: "No invoice requests found" }, { status: 404 }); + } + + return NextResponse.json(invoice, { status: 200 }); + }catch (error) { + return NextResponse.json({ error: "Error :" + error}, { status: 401 }); + } +} \ No newline at end of file diff --git a/app/api/invoice/sent/route.ts b/app/api/invoice/sent/route.ts new file mode 100644 index 0000000..6ea3a43 --- /dev/null +++ b/app/api/invoice/sent/route.ts @@ -0,0 +1,68 @@ +import { supabaseAdmin } from "@/db/connect"; +import { NextResponse, NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; + +export async function GET(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if(!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + // Fetching invoice requests + const {data: invoices, error: dbError} = await supabaseAdmin + .from("invoices") + .select("*") + .eq("sender_gstin", token.gstin) + .eq("status", "requested"); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + if (!invoices || invoices.length === 0) { + return NextResponse.json({ message: "No invoice requests found" }, { status: 404 }); + } + + return NextResponse.json(invoices, { status: 200 }); + }catch (error) { + return NextResponse.json({ error: "Error :" + error}, { status: 401 }); + } +} + +export async function DELETE(req: NextRequest) { + try { + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(req.url); + const invoiceId = searchParams.get("id"); + + if (!invoiceId) { + return NextResponse.json({ error: "Invoice ID is required" }, { status: 400 }); + } + + // Deleting the invoice request + const { error: deleteError } = await supabaseAdmin + .from("invoices") + .delete() + .eq("id", invoiceId) + .eq("sender_gstin", token.gstin); + + if (deleteError) { + return NextResponse.json({ error: "Database error: " + deleteError.message }, { status: 500 }); + } + + return NextResponse.json({ message: "Invoice request deleted successfully" }, { status: 200 }); + } catch (error) { + return NextResponse.json({ error: "Error :" + error }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/transaction/route.ts b/app/api/transaction/route.ts new file mode 100644 index 0000000..9506dae --- /dev/null +++ b/app/api/transaction/route.ts @@ -0,0 +1,33 @@ +import { supabaseAdmin } from "@/db/connect"; +import { NextResponse, NextRequest } from "next/server"; +import { getToken } from "next-auth/jwt"; + +export async function GET(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if(!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Fetching invoice requests + const {data: invoices, error: dbError} = await supabaseAdmin + .from("transactions") + .select("*") + .eq("sender_gstin", token.gstin); + + if (dbError) { + return NextResponse.json({ error: "Database error: " + dbError.message }, { status: 500 }); + } + if (!invoices || invoices.length === 0) { + return NextResponse.json({ message: "No invoice requests found" }, { status: 404 }); + } + + return NextResponse.json(invoices, { status: 200 }); + }catch (error) { + return NextResponse.json({ error: "Error :" + error}, { status: 401 }); + } +} diff --git a/app/api/user/find/route.ts b/app/api/user/find/route.ts new file mode 100644 index 0000000..9f738af --- /dev/null +++ b/app/api/user/find/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getToken } from "next-auth/jwt"; +import { supabaseAdmin } from "@/db/connect"; + +export async function POST(req: NextRequest) { + try{ + // Check authentication using JWT token + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { gstin } = await req.json(); + + if (!gstin) { + return NextResponse.json( + { error: "GSTIN is required" }, + { status: 400 } + ); + } + + // Fetch user by GSTIN + const { data: user, error } = await supabaseAdmin + .from("users") + .select("*") + .eq("gstin", gstin) + .single(); + if(!user){ + return NextResponse.json( + { error: "User not found" }, + { status: 404 } + ); + } + if (error) { + console.error("Error fetching user:", error); + return NextResponse.json( + { error: "Failed to fetch user. The GSTIN could be incorrect." }, + { status: 404 } + ); + } + // Only send necessary user details + const userResponse = { + user_id: user.user_id, + gstin: user.gstin, + business_name: user.business_name, + business_email: user.business_email, + business_address: user.business_address, + business_description: user.business_description, + mobile_number: user.mobile_number, + profile_url: user.profile_url + }; + + return NextResponse.json({ user: userResponse }, { status: 200 }); + } + catch (error) { + console.error("Error in POST request:", error); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/user/update/route.ts b/app/api/user/update/route.ts new file mode 100644 index 0000000..a496902 --- /dev/null +++ b/app/api/user/update/route.ts @@ -0,0 +1,106 @@ +//https://fxlianxlwekzkiqarlev.supabase.co/storage/v1/object/public/profile-pictures/ + +import { NextRequest, NextResponse } from "next/server"; +import { getToken } from "next-auth/jwt"; +import { supabaseAdmin } from "@/db/connect"; + +export async function POST(req: NextRequest) { + try{ + const token = await getToken({ + req, + secret: process.env.NEXTAUTH_SECRET, + }); + + if (!token || !token.gstin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const formData = await req.formData(); + + const picture = formData.get("profilePicture") as File | null; + const user_id = formData.get("userId") as string; + const description = formData.get("description") as string; + const email = formData.get("email") as string; + + if (!user_id) { + return NextResponse.json( + { error: "User ID is required." }, + { status: 400 } + ); + } + + // Fetch user by user_id + const { data: user, error: userFetchError } = await supabaseAdmin + .from("users") + .select("*") + .eq("user_id", user_id) + .single(); + + if (userFetchError || !user) { + return NextResponse.json( + { error: "User not found." }, + { status: 404 } + ); + } + + let filePath = user.profile_url; // Keep existing profile URL by default + + // Handle file upload if a new picture is provided + if (picture && picture.size > 0) { + const fileExtension = picture.name.split(".").pop()?.toLowerCase(); + + if (!fileExtension || !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension)) { + return NextResponse.json( + { error: "Invalid file type. Only images are allowed." }, + { status: 400 } + ); + } + + const fileName = `${user_id}.${fileExtension}`; + filePath = `https://fxlianxlwekzkiqarlev.supabase.co/storage/v1/object/public/profile-pictures/${fileName}`; + + // Upload file to Supabase storage + const { error: uploadError } = await supabaseAdmin.storage + .from("profile-pictures") + .upload(fileName, picture, { + upsert: true // This will overwrite existing file with same name + }); + + if (uploadError) { + console.error("File upload error:", uploadError); + return NextResponse.json( + { error: "File upload failed: " + uploadError.message }, + { status: 500 } + ); + } + } + + // Update user profile in database + const { error: updateError } = await supabaseAdmin + .from("users") + .update({ + profile_url: filePath, + business_description: description || user.business_description, + business_email: email || user.business_email + }) + .eq("user_id", user_id); + + if (updateError) { + console.error("Error updating user:", updateError); + return NextResponse.json( + { error: "Failed to update user profile: " + updateError.message }, + { status: 500 } + ); + } + + return NextResponse.json({ + message: "Profile updated successfully!", + profile_url: filePath + }, { status: 200 }); + } + catch (error) { + console.error("Error in POST request:", error); + return NextResponse.json({ + error: "Internal Server Error: " + (error instanceof Error ? error.message : "Unknown error") + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/user/verify/route.ts b/app/api/user/verify/route.ts new file mode 100644 index 0000000..1d63646 --- /dev/null +++ b/app/api/user/verify/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabaseAdmin } from "@/db/connect"; + +export async function POST(request: NextRequest) { + try { + const { gstin } = await request.json(); + if (!gstin) { + return NextResponse.json({ error: "GSTIN is required" }, { status: 400 }); + } + + // Fetch user by GSTIN + const { data: recipient, error } = await supabaseAdmin + .from("key-attributes") + .select("public_key") + .eq("gstin", gstin) + .single(); + if (!recipient) { + return NextResponse.json( + { error: "Recipient not found." }, + { status: 404 } + ); + } + if (error) { + console.error("Error fetching recipient information: ", error); + return NextResponse.json( + { error: "Error while fetching user details. Try later." }, + { status: 404 } + ); + } + // Only send necessary recipient details + const userResponse = { + publicKey: recipient.public_key, + gstin: gstin, + }; + + return NextResponse.json({ recipient: userResponse }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: "An error occurred while processing your request." }, + { status: 500 } + ); + } +} \ No newline at end of file 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/dashboard/invoices/[invoice_id]/layout.tsx b/app/dashboard/invoices/[invoice_id]/layout.tsx new file mode 100644 index 0000000..ba72011 --- /dev/null +++ b/app/dashboard/invoices/[invoice_id]/layout.tsx @@ -0,0 +1,7 @@ +export default function InvoiceLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/app/dashboard/invoices/[invoice_id]/page.tsx b/app/dashboard/invoices/[invoice_id]/page.tsx index 5120271..fae9cf1 100644 --- a/app/dashboard/invoices/[invoice_id]/page.tsx +++ b/app/dashboard/invoices/[invoice_id]/page.tsx @@ -1,8 +1,172 @@ +'use client'; + +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { useAlertActions } from "@/lib/use-alert"; +import { FetchedInvoice } from "@/db/types/fetched"; +import { decryptPdf, decryptPrimaryInvoiceKey, decryoptSecondaryInvoiceKey } from "@/lib/crypto"; export default function Home() { + // const router = useRouter(); + const { invoice_id } = useParams(); + const { data: session } = useSession(); + const [senderId, setSenderId] = useState(null) + const { showError, showSuccess, showWarning } = useAlertActions(); + const [invoiceDetails, setInvoiceDetails] = useState(null); + const [invoiceFile, setInvoiceFile] = useState(null); + // const [encryptedFile, setEncryptedFile] = useState(); + const [isFetching, setIsFetching] = useState(false); + + const gstin = session?.user?.gstin || ""; + + async function fetchEncryptedBlob() { + if(!senderId)return; + + const data = await fetch(`${process.env.NEXT_STORAGE_URL}/${senderId}/${invoice_id}.enc`); + const encrypted = await data.blob(); + return encrypted; + // Convert Blob to File before setting state + // const file = new File([encrypted], `${invoice_id}.enc`, { type: encrypted.type }); + // setEncryptedFile(file); + } + + + async function decryptInvoiceFile() { + const encryptedBlob = await fetchEncryptedBlob(); + if(!encryptedBlob)return; + + const arrayBuffer = await encryptedBlob.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + const header = invoiceDetails?.decryption_header; + if (!header) { + showError("Decryption header not found for the invoice."); + return; + } + const masterKey = sessionStorage.getItem('masterKey'); + if(!masterKey){ + showError("Master key not found in session. Please login again."); + return; + } + const keyAttributes = JSON.parse(localStorage.getItem("keyAttributes") || "{}"); + if (!keyAttributes || Object.keys(keyAttributes).length === 0) { + showError("Key attributes not found. Please verify your GST number again."); + return; + } + // If the user is sender, use primary key else use secondary key + if (invoiceDetails?.sender_gstin === gstin) { + // Decrypt using primary key + const primaryKey = invoiceDetails?.primary_invoice_key; + if(!primaryKey){ + showError("Primary invoice key not found."); + return; + } + const invoiceKey = await decryptPrimaryInvoiceKey(primaryKey, masterKey); + if(!invoiceKey){ + showError("Failed to decrypt invoice key. Please check your master key."); + return; + } + + const decryptedFile = await decryptPdf(uint8Array, header, invoiceKey); + if(!decryptedFile){ + showError("Failed to decrypt invoice file."); + return; + } + const file = new File([decryptedFile.slice(0)], `${invoice_id}.pdf`, { type: 'application/pdf' }); + if(!file){ + showError("Failed to create file from decrypted data."); + return; + } + setInvoiceFile(file); + showSuccess("Invoice file decrypted successfully."); + } else if (invoiceDetails?.recipient_gstin === gstin){ + // Decrypt using secondary key + const secondaryKey = invoiceDetails?.secondary_invoice_key; + if(!secondaryKey){ + showError("Secondary invoice key not found."); + return; + } + const recipientPrivateKey = sessionStorage.getItem('secretKey'); + const recipientPublicKey = keyAttributes.publicKey; + if(!recipientPrivateKey || !recipientPublicKey){ + showError("Recipient keys not found in session. Please login again."); + return; + } + + const invoiceKey = await decryoptSecondaryInvoiceKey(secondaryKey, recipientPrivateKey, recipientPublicKey); + if(!invoiceKey){ + showError("Failed to decrypt invoice key. Please check your keys."); + return; + } + const decryptedFile = await decryptPdf(uint8Array, header, invoiceKey); + if(!decryptedFile){ + showError("Failed to decrypt invoice file."); + return; + } + const file = new File([decryptedFile.slice(0)], `${invoice_id}.pdf`, { type: 'application/pdf' }); + if(!file){ + showError("Failed to create file from decrypted data."); + return; + } + setInvoiceFile(file); + showSuccess("Invoice file decrypted successfully."); + }else{ + showError("You are not authorized to access the file."); + } + } + async function getInvoiceDetails() { + // Fetch invoice details from your API + console.log(invoice_id); + console.log(gstin); + const res = await fetch("/api/invoice", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + invoice_id : invoice_id, + gstin: gstin, + }) + }); + const data = await res.json(); + setInvoiceDetails(data); + } + + useEffect(() => { + if (!session || !gstin || !invoice_id) return; + async function fetchInvoiceDetails() { + await getInvoiceDetails(); + } + const timeoutId = setTimeout(() => { + fetchInvoiceDetails(); + }, 3000); + return () => clearTimeout(timeoutId); + }, [session, gstin, invoice_id]); return (
-
-

Invoice number - 5468465

+
+
+ {invoiceFile ? ( + + ) : + ( +
+

Decrypting invoice file...

+
+ )} +
+
+
+

Invoice actions

+
+
+

Invoice details

+
+
); diff --git a/app/dashboard/invoices/page.tsx b/app/dashboard/invoices/page.tsx index 00d99cc..c8715ad 100644 --- a/app/dashboard/invoices/page.tsx +++ b/app/dashboard/invoices/page.tsx @@ -1,179 +1,8 @@ -"use client" -import Link from "next/link" -import { useState } from "react" -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 { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" +"use client"; -export default function Page() { - const [open, setOpen] = useState(false) - const [date, setDate] = useState(undefined) - const senderGstNumber = "27AAACF1234A1Z5" // Example GST number +import { InvoiceForm } from "@/components/invoices/InvoiceForm"; - const [invoice, setInvoice] = useState(null) - const [receiverGstNumber, setReceiverGstNumber] = useState("") - const [amount, setAmount] = useState("") - const [title, setTitle] = useState("") - const [invoiceNumber, setInvoiceNumber] = useState("") - const [description, setDescription] = useState("") - // const [verified, setVerified] = useState(false) - return ( -
- - - Generate Invoice - - First enter GST number to and click verify, then fill out the invoice details. - - - - - - - {/* Form will be hidden until GST number is verified */} - {/*
*/} - -
-
- -
- -
-
-
- -
- setReceiverGstNumber(e.target.value)} - /> - {/* */} -
-
+export default function Page() { + return ; +} - {/*
- -
*/} -
-
-
- - { - if (e.target.files && e.target.files[0]) { - setInvoice(e.target.files[0]) - } - }} - id="invoice" type="file" required/> -
-
- - - - - - - { - setDate(date) - setOpen(false) - }} - /> - - -
-
- - setAmount(e.target.value)} - /> -
-
-
-
- - setTitle(e.target.value)} - /> -
-
- - setInvoiceNumber(e.target.value)} - /> -
-
-
- -