diff --git a/src/app/api/auth/password-reset/request/route.ts b/src/app/api/auth/password-reset/request/route.ts index 17ad14f..16db5cb 100644 --- a/src/app/api/auth/password-reset/request/route.ts +++ b/src/app/api/auth/password-reset/request/route.ts @@ -4,6 +4,7 @@ import { sendEmail } from "@/lib/email" import { emailSchema } from "@/lib/validation" import { applyRateLimit } from "@/lib/rate-limit" import { handleApiError } from "@/lib/errors" +import { getBaseUrl } from "@/lib/utils" import crypto from "crypto" export async function POST(req: Request) { @@ -64,8 +65,8 @@ export async function POST(req: Request) { }) // Send reset email - const resetUrl = `${process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/reset-password?token=${resetToken}&email=${encodeURIComponent(email)}` - + const resetUrl = `${getBaseUrl()}/reset-password?token=${resetToken}&email=${encodeURIComponent(email)}` + const emailHtml = ` diff --git a/src/app/api/auth/verify-email/send/route.ts b/src/app/api/auth/verify-email/send/route.ts index f8af86c..8fb221d 100644 --- a/src/app/api/auth/verify-email/send/route.ts +++ b/src/app/api/auth/verify-email/send/route.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma" import { sendEmail } from "@/lib/email" import { applyRateLimit } from "@/lib/rate-limit" import { handleApiError } from "@/lib/errors" +import { getBaseUrl } from "@/lib/utils" import crypto from "crypto" export async function POST(req: Request) { @@ -63,8 +64,8 @@ export async function POST(req: Request) { }) // Send verification email - const verifyUrl = `${process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/verify-email?token=${verificationToken}&email=${encodeURIComponent(user.email)}` - + const verifyUrl = `${getBaseUrl()}/verify-email?token=${verificationToken}&email=${encodeURIComponent(user.email)}` + const emailHtml = ` diff --git a/src/app/api/integrations/gmail/callback/route.ts b/src/app/api/integrations/gmail/callback/route.ts index 2597f4b..d073192 100644 --- a/src/app/api/integrations/gmail/callback/route.ts +++ b/src/app/api/integrations/gmail/callback/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" +import { getBaseUrl } from "@/lib/utils" /** * GET /api/integrations/gmail/callback @@ -10,13 +11,8 @@ export async function GET(req: Request) { try { const session = await auth() if (!session?.user?.id) { - const baseUrl = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/login?error=unauthorized", baseUrl) + new URL("/login?error=unauthorized", getBaseUrl()) ) } @@ -28,38 +24,23 @@ export async function GET(req: Request) { // Handle OAuth errors if (error) { console.error("Gmail OAuth error:", error) - const baseUrl = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?error=oauth_failed", baseUrl) + new URL("/dashboard/juno-engine/gmail-connect?error=oauth_failed", getBaseUrl()) ) } if (!code) { - const baseUrl = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?error=no_code", baseUrl) + new URL("/dashboard/juno-engine/gmail-connect?error=no_code", getBaseUrl()) ) } // Exchange authorization code for tokens // IMPORTANT: This redirect URI must exactly match what was sent in the authorization request - const baseUrl = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" - const redirectUri = `${baseUrl}/api/integrations/gmail/callback` - + const redirectUri = `${getBaseUrl()}/api/integrations/gmail/callback` + console.log("Gmail OAuth Callback - Redirect URI:", redirectUri) // Log for debugging - + const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { method: "POST", headers: { @@ -78,29 +59,19 @@ export async function GET(req: Request) { const errorData = await tokenResponse.json().catch(() => ({})) console.error("Token exchange error:", errorData) console.error("Redirect URI used:", redirectUri) - + // Provide more specific error message if it's a redirect_uri_mismatch if (errorData.error === "redirect_uri_mismatch") { - const baseUrlForError = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( new URL( `/dashboard/juno-engine/gmail-connect?error=redirect_uri_mismatch&redirect_uri=${encodeURIComponent(redirectUri)}`, - baseUrlForError + getBaseUrl() ) ) } - - const baseUrlForError = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" + return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?error=token_exchange_failed", baseUrlForError) + new URL("/dashboard/juno-engine/gmail-connect?error=token_exchange_failed", getBaseUrl()) ) } @@ -109,7 +80,7 @@ export async function GET(req: Request) { // Get user's Gmail email address // Try userinfo endpoint first (requires email/profile scopes) let userInfo: { email: string } | null = null - + try { const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", { headers: { @@ -142,25 +113,15 @@ export async function GET(req: Request) { } } catch (error) { console.error("Error fetching user info:", error) - const baseUrlForError = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?error=user_info_failed", baseUrlForError) + new URL("/dashboard/juno-engine/gmail-connect?error=user_info_failed", getBaseUrl()) ) } if (!userInfo?.email) { console.error("No email found in user info") - const baseUrlForError = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?error=user_info_failed", baseUrlForError) + new URL("/dashboard/juno-engine/gmail-connect?error=user_info_failed", getBaseUrl()) ) } @@ -190,23 +151,13 @@ export async function GET(req: Request) { } }) - const baseUrlDone = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?connected=true", baseUrlDone) + new URL("/dashboard/juno-engine/gmail-connect?connected=true", getBaseUrl()) ) } catch (error) { console.error("Gmail callback error:", error) - const baseUrlError = - process.env.AUTH_URL || - process.env.NEXTAUTH_URL || - process.env.NEXT_PUBLIC_APP_URL || - "http://localhost:3000" return NextResponse.redirect( - new URL("/dashboard/juno-engine/gmail-connect?error=callback_failed", baseUrlError) + new URL("/dashboard/juno-engine/gmail-connect?error=callback_failed", getBaseUrl()) ) } } diff --git a/src/app/api/vendors/suppliers/[supplierId]/invite/route.ts b/src/app/api/vendors/suppliers/[supplierId]/invite/route.ts index 2b21221..b308735 100644 --- a/src/app/api/vendors/suppliers/[supplierId]/invite/route.ts +++ b/src/app/api/vendors/suppliers/[supplierId]/invite/route.ts @@ -3,6 +3,7 @@ import { auth } from "@/auth" import { prisma } from "@/lib/prisma" import { sendEmail, generateInvitationEmailHtml } from "@/lib/email" import { notifyInvitationReceived } from "@/lib/notifications" +import { getBaseUrl } from "@/lib/utils" import crypto from "crypto" /** @@ -39,8 +40,8 @@ export async function DELETE( ) } - const storeIdsToCheck = storeId - ? [storeId] + const storeIdsToCheck = storeId + ? [storeId] : vendorStores.map(s => s.id) // Find and delete pending invitations @@ -122,8 +123,8 @@ export async function POST( } // Determine which stores to connect - const storesToConnect = connectToAllStores - ? vendorStores + const storesToConnect = connectToAllStores + ? vendorStores : vendorStores.filter(store => store.isActive) if (storesToConnect.length === 0) { @@ -203,8 +204,8 @@ export async function POST( if (storesToInvite.length === 0) { const allConnected = storesToConnect.length === existingConnections.length + existingInvitations.length return NextResponse.json( - { - message: allConnected + { + message: allConnected ? "All selected stores are already connected or have pending invitations" : "No stores available to invite (all have existing connections or pending invitations)" }, @@ -221,7 +222,7 @@ export async function POST( for (const store of storesToInvite) { const invitationToken = crypto.randomBytes(32).toString("hex").toUpperCase().slice(0, 16) - + try { const invitation = await (prisma as any).invitation.create({ data: { @@ -248,8 +249,8 @@ export async function POST( // Send invitation email (use first store's name or combined names) const primaryStore = storesToInvite[0] - const invitationLink = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/supplier/vendors?token=${invitations[0].token}` - + const invitationLink = `${getBaseUrl()}/supplier/vendors?token=${invitations[0].token}` + const emailSubject = connectToAllStores && storesToInvite.length > 1 ? `Invitation to Connect - ${storesToInvite.length} Stores` : `Invitation to Connect - ${primaryStore.businessName}` @@ -273,10 +274,10 @@ export async function POST( // Create notification for supplier about the invitation try { const vendorName = session.user.name || storesToInvite[0].businessName - const storeDisplayName = connectToAllStores && storesToInvite.length > 1 - ? `${storesToInvite.length} Stores` + const storeDisplayName = connectToAllStores && storesToInvite.length > 1 + ? `${storesToInvite.length} Stores` : primaryStore.businessName - + await notifyInvitationReceived( supplierProfile.user.id, vendorName, @@ -307,9 +308,9 @@ export async function POST( const errorStack = error instanceof Error ? error.stack : undefined console.error("Error details:", { errorMessage, errorStack }) return NextResponse.json( - { + { message: "Failed to send invitation", - error: errorMessage + error: errorMessage }, { status: 500 } ) diff --git a/src/app/api/webhooks/gmail/route.ts b/src/app/api/webhooks/gmail/route.ts index e8f41d7..ef0f2f8 100644 --- a/src/app/api/webhooks/gmail/route.ts +++ b/src/app/api/webhooks/gmail/route.ts @@ -37,7 +37,7 @@ export async function POST(req: NextRequest) { // Decode base64url encoded message data const decodedData = Buffer.from(messageData, "base64") .toString("utf-8") - + const notification = JSON.parse(decodedData) const emailAddress = notification.emailAddress const historyId = notification.historyId @@ -74,6 +74,19 @@ export async function POST(req: NextRequest) { ) } + // 🛡️ SECURITY: Only allow VENDORS to use automated replies + // Suppliers should not send automated responses via JUNOHUB + if (integration.user.role !== "VENDOR") { + logger.info("Skipping Gmail processing for non-vendor role", { + emailAddress, + role: integration.user.role + }) + return NextResponse.json({ + success: true, + message: "Skipping processing for non-vendor account" + }) + } + const userId = integration.userId const storeId = integration.user.stores[0]?.id || null @@ -89,7 +102,7 @@ export async function POST(req: NextRequest) { error: result.error, }) - return NextResponse.json({ + return NextResponse.json({ success: result.success, error: result.error, message: result.success @@ -112,7 +125,7 @@ export async function POST(req: NextRequest) { * Health check endpoint */ export async function GET() { - return NextResponse.json({ + return NextResponse.json({ status: "ok", message: "Gmail webhook endpoint is active" }) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 03aaa4b..47bf78b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,10 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function getBaseUrl() { + if (process.env.AUTH_URL) return process.env.AUTH_URL + if (process.env.NEXTAUTH_URL) return process.env.NEXTAUTH_URL + if (process.env.NEXT_PUBLIC_APP_URL) return process.env.NEXT_PUBLIC_APP_URL + return "http://localhost:3000" +}