Reset password fixed & supplier gmail restrictions
This commit is contained in:
parent
bfd858ca62
commit
aada21d0a7
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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"
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user