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() {
Loading...
; + // Get icon based on variant + const getAlertIcon = () => { + switch (alert.variant) { + case 'destructive': + return+ Search for users by their GST number to view their business profile and information. +
+Found user matching your search criteria
+Enter a GST number above to find user information
+Loading...
; + // Get icon based on variant + const getAlertIcon = () => { + switch (alert.variant) { + case 'destructive': + returnSubsribe to our newsletter to stay updated on our progress and be the first to know when we launch!
-{error}
+Do't worry, we won't spam your inbox • Cancel anytime
Loading...
; return (+ {user.desc} +
++ {user.address} +
++ GST: {user.gstin} +
++ {user.business_description} +
: ++ No description provided. +
+ } ++ {user.business_address} +
+