Reset password fixed & supplier gmail restrictions

This commit is contained in:
root 2026-02-16 11:50:15 +05:00
parent bfd858ca62
commit aada21d0a7
6 changed files with 60 additions and 86 deletions

View File

@ -4,6 +4,7 @@ import { sendEmail } from "@/lib/email"
import { emailSchema } from "@/lib/validation" import { emailSchema } from "@/lib/validation"
import { applyRateLimit } from "@/lib/rate-limit" import { applyRateLimit } from "@/lib/rate-limit"
import { handleApiError } from "@/lib/errors" import { handleApiError } from "@/lib/errors"
import { getBaseUrl } from "@/lib/utils"
import crypto from "crypto" import crypto from "crypto"
export async function POST(req: Request) { export async function POST(req: Request) {
@ -64,8 +65,8 @@ export async function POST(req: Request) {
}) })
// Send reset email // 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 = ` const emailHtml = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"
import { sendEmail } from "@/lib/email" import { sendEmail } from "@/lib/email"
import { applyRateLimit } from "@/lib/rate-limit" import { applyRateLimit } from "@/lib/rate-limit"
import { handleApiError } from "@/lib/errors" import { handleApiError } from "@/lib/errors"
import { getBaseUrl } from "@/lib/utils"
import crypto from "crypto" import crypto from "crypto"
export async function POST(req: Request) { export async function POST(req: Request) {
@ -63,8 +64,8 @@ export async function POST(req: Request) {
}) })
// Send verification email // 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 = ` const emailHtml = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -1,6 +1,7 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { auth } from "@/auth" import { auth } from "@/auth"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { getBaseUrl } from "@/lib/utils"
/** /**
* GET /api/integrations/gmail/callback * GET /api/integrations/gmail/callback
@ -10,13 +11,8 @@ export async function GET(req: Request) {
try { try {
const session = await auth() const session = await auth()
if (!session?.user?.id) { 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( 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 // Handle OAuth errors
if (error) { if (error) {
console.error("Gmail OAuth error:", 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( 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) { if (!code) {
const baseUrl =
process.env.AUTH_URL ||
process.env.NEXTAUTH_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
"http://localhost:3000"
return NextResponse.redirect( 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 // Exchange authorization code for tokens
// IMPORTANT: This redirect URI must exactly match what was sent in the authorization request // IMPORTANT: This redirect URI must exactly match what was sent in the authorization request
const baseUrl = const redirectUri = `${getBaseUrl()}/api/integrations/gmail/callback`
process.env.AUTH_URL ||
process.env.NEXTAUTH_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
"http://localhost:3000"
const redirectUri = `${baseUrl}/api/integrations/gmail/callback`
console.log("Gmail OAuth Callback - Redirect URI:", redirectUri) // Log for debugging console.log("Gmail OAuth Callback - Redirect URI:", redirectUri) // Log for debugging
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
method: "POST", method: "POST",
headers: { headers: {
@ -78,29 +59,19 @@ export async function GET(req: Request) {
const errorData = await tokenResponse.json().catch(() => ({})) const errorData = await tokenResponse.json().catch(() => ({}))
console.error("Token exchange error:", errorData) console.error("Token exchange error:", errorData)
console.error("Redirect URI used:", redirectUri) console.error("Redirect URI used:", redirectUri)
// Provide more specific error message if it's a redirect_uri_mismatch // Provide more specific error message if it's a redirect_uri_mismatch
if (errorData.error === "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( return NextResponse.redirect(
new URL( new URL(
`/dashboard/juno-engine/gmail-connect?error=redirect_uri_mismatch&redirect_uri=${encodeURIComponent(redirectUri)}`, `/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( 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 // Get user's Gmail email address
// Try userinfo endpoint first (requires email/profile scopes) // Try userinfo endpoint first (requires email/profile scopes)
let userInfo: { email: string } | null = null let userInfo: { email: string } | null = null
try { try {
const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", { const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
headers: { headers: {
@ -142,25 +113,15 @@ export async function GET(req: Request) {
} }
} catch (error) { } catch (error) {
console.error("Error fetching user info:", 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( 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) { if (!userInfo?.email) {
console.error("No email found in user info") 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( 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( 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) { } catch (error) {
console.error("Gmail callback error:", 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( 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())
) )
} }
} }

View File

@ -3,6 +3,7 @@ import { auth } from "@/auth"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { sendEmail, generateInvitationEmailHtml } from "@/lib/email" import { sendEmail, generateInvitationEmailHtml } from "@/lib/email"
import { notifyInvitationReceived } from "@/lib/notifications" import { notifyInvitationReceived } from "@/lib/notifications"
import { getBaseUrl } from "@/lib/utils"
import crypto from "crypto" import crypto from "crypto"
/** /**
@ -39,8 +40,8 @@ export async function DELETE(
) )
} }
const storeIdsToCheck = storeId const storeIdsToCheck = storeId
? [storeId] ? [storeId]
: vendorStores.map(s => s.id) : vendorStores.map(s => s.id)
// Find and delete pending invitations // Find and delete pending invitations
@ -122,8 +123,8 @@ export async function POST(
} }
// Determine which stores to connect // Determine which stores to connect
const storesToConnect = connectToAllStores const storesToConnect = connectToAllStores
? vendorStores ? vendorStores
: vendorStores.filter(store => store.isActive) : vendorStores.filter(store => store.isActive)
if (storesToConnect.length === 0) { if (storesToConnect.length === 0) {
@ -203,8 +204,8 @@ export async function POST(
if (storesToInvite.length === 0) { if (storesToInvite.length === 0) {
const allConnected = storesToConnect.length === existingConnections.length + existingInvitations.length const allConnected = storesToConnect.length === existingConnections.length + existingInvitations.length
return NextResponse.json( return NextResponse.json(
{ {
message: allConnected message: allConnected
? "All selected stores are already connected or have pending invitations" ? "All selected stores are already connected or have pending invitations"
: "No stores available to invite (all have existing connections or 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) { for (const store of storesToInvite) {
const invitationToken = crypto.randomBytes(32).toString("hex").toUpperCase().slice(0, 16) const invitationToken = crypto.randomBytes(32).toString("hex").toUpperCase().slice(0, 16)
try { try {
const invitation = await (prisma as any).invitation.create({ const invitation = await (prisma as any).invitation.create({
data: { data: {
@ -248,8 +249,8 @@ export async function POST(
// Send invitation email (use first store's name or combined names) // Send invitation email (use first store's name or combined names)
const primaryStore = storesToInvite[0] 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 const emailSubject = connectToAllStores && storesToInvite.length > 1
? `Invitation to Connect - ${storesToInvite.length} Stores` ? `Invitation to Connect - ${storesToInvite.length} Stores`
: `Invitation to Connect - ${primaryStore.businessName}` : `Invitation to Connect - ${primaryStore.businessName}`
@ -273,10 +274,10 @@ export async function POST(
// Create notification for supplier about the invitation // Create notification for supplier about the invitation
try { try {
const vendorName = session.user.name || storesToInvite[0].businessName const vendorName = session.user.name || storesToInvite[0].businessName
const storeDisplayName = connectToAllStores && storesToInvite.length > 1 const storeDisplayName = connectToAllStores && storesToInvite.length > 1
? `${storesToInvite.length} Stores` ? `${storesToInvite.length} Stores`
: primaryStore.businessName : primaryStore.businessName
await notifyInvitationReceived( await notifyInvitationReceived(
supplierProfile.user.id, supplierProfile.user.id,
vendorName, vendorName,
@ -307,9 +308,9 @@ export async function POST(
const errorStack = error instanceof Error ? error.stack : undefined const errorStack = error instanceof Error ? error.stack : undefined
console.error("Error details:", { errorMessage, errorStack }) console.error("Error details:", { errorMessage, errorStack })
return NextResponse.json( return NextResponse.json(
{ {
message: "Failed to send invitation", message: "Failed to send invitation",
error: errorMessage error: errorMessage
}, },
{ status: 500 } { status: 500 }
) )

View File

@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
// Decode base64url encoded message data // Decode base64url encoded message data
const decodedData = Buffer.from(messageData, "base64") const decodedData = Buffer.from(messageData, "base64")
.toString("utf-8") .toString("utf-8")
const notification = JSON.parse(decodedData) const notification = JSON.parse(decodedData)
const emailAddress = notification.emailAddress const emailAddress = notification.emailAddress
const historyId = notification.historyId 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 userId = integration.userId
const storeId = integration.user.stores[0]?.id || null const storeId = integration.user.stores[0]?.id || null
@ -89,7 +102,7 @@ export async function POST(req: NextRequest) {
error: result.error, error: result.error,
}) })
return NextResponse.json({ return NextResponse.json({
success: result.success, success: result.success,
error: result.error, error: result.error,
message: result.success message: result.success
@ -112,7 +125,7 @@ export async function POST(req: NextRequest) {
* Health check endpoint * Health check endpoint
*/ */
export async function GET() { export async function GET() {
return NextResponse.json({ return NextResponse.json({
status: "ok", status: "ok",
message: "Gmail webhook endpoint is active" message: "Gmail webhook endpoint is active"
}) })

View File

@ -4,3 +4,10 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) 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"
}