email logo fixed and timeout retry for validation of shopify credentials

This commit is contained in:
root 2026-02-26 11:33:45 +05:00 committed by talatops
parent d1a1dfc4f5
commit fadec2e06f
7 changed files with 159 additions and 44 deletions

View File

@ -7,7 +7,10 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"re-embed-knowledge-base": "tsx scripts/re-embed-knowledge-base.ts" "re-embed-knowledge-base": "tsx scripts/re-embed-knowledge-base.ts",
"db:reset": "prisma migrate reset",
"db:studio": "prisma studio",
"db:fix-migration": "tsx scripts/fix-failed-migration.ts"
}, },
"dependencies": { "dependencies": {
"@auth/core": "^0.41.0", "@auth/core": "^0.41.0",

View File

@ -0,0 +1,69 @@
#!/usr/bin/env tsx
/**
* Fix Prisma failed migration (P3009) when database has partial schema.
*
* Use when: migrate deploy fails with "UserRole already exists" or similar,
* and Prisma reports a failed migration that blocks new migrations.
*
* This script:
* 1. Marks the failed migration as rolled back
* 2. Drops and recreates the public schema (clears all objects)
* 3. Re-applies migrations
*
* Usage: npx tsx scripts/fix-failed-migration.ts
*
* Requires: DATABASE_URL in .env (e.g. postgresql://postgres:password@localhost:5444/dropshipping)
*/
import "dotenv/config"
import { execSync } from "child_process"
import { Client } from "pg"
const MIGRATION_NAME = "20260216152711_init_schema"
async function main() {
const dbUrl = process.env.DATABASE_URL
if (!dbUrl) {
console.error("DATABASE_URL is not set in .env")
process.exit(1)
}
console.log("Step 1: Marking failed migration as rolled back...")
try {
execSync(`npx prisma migrate resolve --rolled-back ${MIGRATION_NAME}`, {
stdio: "inherit",
env: { ...process.env, DATABASE_URL: dbUrl },
})
console.log(" ✓ Migration marked as rolled back")
} catch (e) {
// May fail if migration record doesn't exist - continue anyway
console.log(" (Migration resolve completed or migration not in failed state)")
}
console.log("\nStep 2: Dropping and recreating public schema...")
const client = new Client({ connectionString: dbUrl })
await client.connect()
try {
await client.query("DROP SCHEMA public CASCADE")
await client.query("CREATE SCHEMA public")
await client.query("GRANT ALL ON SCHEMA public TO postgres")
await client.query("GRANT ALL ON SCHEMA public TO public")
await client.query("CREATE EXTENSION IF NOT EXISTS vector")
console.log(" ✓ Schema reset complete")
} finally {
await client.end()
}
console.log("\nStep 3: Applying migrations...")
execSync("npx prisma migrate deploy", {
stdio: "inherit",
env: { ...process.env, DATABASE_URL: dbUrl },
})
console.log("\n✓ Migration fix complete. Database is ready.")
}
main().catch((err) => {
console.error("Fix failed:", err)
process.exit(1)
})

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { sendEmail } from "@/lib/email" import { sendEmail, getEmailLogoUrl } 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"
@ -80,6 +80,16 @@ export async function POST(req: Request) {
<tr> <tr>
<td style="padding: 40px 20px; text-align: center;"> <td style="padding: 40px 20px; text-align: center;">
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #1e293b; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden;"> <table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #1e293b; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden;">
<tr>
<td style="padding: 40px 40px 30px; background: linear-gradient(135deg, rgba(79, 70, 229, 0.2) 0%, rgba(99, 102, 241, 0.1) 100%); border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="text-align: center;">
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 16px;">
<img src="${getEmailLogoUrl()}" alt="JUNOHUB" width="48" height="48" style="display: block; border-radius: 12px; object-fit: contain;" />
<h1 style="margin: 0; font-size: 28px; font-weight: 800; color: #ffffff; letter-spacing: -0.5px;">JUNOHUB</h1>
</div>
</div>
</td>
</tr>
<tr> <tr>
<td style="padding: 40px;"> <td style="padding: 40px;">
<h2 style="margin: 0 0 16px; font-size: 24px; font-weight: 700; color: #ffffff;">Reset Your Password</h2> <h2 style="margin: 0 0 16px; font-size: 24px; font-weight: 700; color: #ffffff;">Reset Your Password</h2>

View File

@ -1,7 +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 { sendEmail } from "@/lib/email" import { sendEmail, getEmailLogoUrl } 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 { getBaseUrl } from "@/lib/utils"
@ -79,6 +79,16 @@ export async function POST(req: Request) {
<tr> <tr>
<td style="padding: 40px 20px; text-align: center;"> <td style="padding: 40px 20px; text-align: center;">
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #1e293b; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden;"> <table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #1e293b; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden;">
<tr>
<td style="padding: 40px 40px 30px; background: linear-gradient(135deg, rgba(79, 70, 229, 0.2) 0%, rgba(99, 102, 241, 0.1) 100%); border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="text-align: center;">
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 16px;">
<img src="${getEmailLogoUrl()}" alt="JUNOHUB" width="48" height="48" style="display: block; border-radius: 12px; object-fit: contain;" />
<h1 style="margin: 0; font-size: 28px; font-weight: 800; color: #ffffff; letter-spacing: -0.5px;">JUNOHUB</h1>
</div>
</div>
</td>
</tr>
<tr> <tr>
<td style="padding: 40px;"> <td style="padding: 40px;">
<h2 style="margin: 0 0 16px; font-size: 24px; font-weight: 700; color: #ffffff;">Verify Your Email</h2> <h2 style="margin: 0 0 16px; font-size: 24px; font-weight: 700; color: #ffffff;">Verify Your Email</h2>

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import { sendEmail } from "@/lib/email" import { sendEmail, getEmailLogoUrl } from "@/lib/email"
import { logger } from "@/lib/logger" import { logger } from "@/lib/logger"
// This is a simple mock-up of a newsletter subscription API // This is a simple mock-up of a newsletter subscription API
@ -63,11 +63,7 @@ function generateNewsletterWelcomeHtml(email: string): string {
<td style="padding: 50px 40px 30px; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);"> <td style="padding: 50px 40px 30px; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);">
<div style="text-align: center;"> <div style="text-align: center;">
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 20px;"> <div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 20px;">
<div style="width: 56px; height: 56px; background-color: rgba(255, 255, 255, 0.2); border-radius: 16px; display: inline-block; vertical-align: middle;"> <img src="${getEmailLogoUrl()}" alt="JUNOHUB" width="56" height="56" style="display: block; border-radius: 16px; object-fit: contain;" />
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-top: 12px;">
<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" fill="white"/>
</svg>
</div>
<h1 style="margin: 0; font-size: 32px; font-weight: 900; color: #ffffff; letter-spacing: -1px; display: inline-block; vertical-align: middle; margin-left: 10px;">JUNOHUB</h1> <h1 style="margin: 0; font-size: 32px; font-weight: 900; color: #ffffff; letter-spacing: -1px; display: inline-block; vertical-align: middle; margin-left: 10px;">JUNOHUB</h1>
</div> </div>
</div> </div>

View File

@ -1,5 +1,19 @@
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
const TRANSIENT_ERROR_CODES = ["ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]
const MAX_RETRIES = 3
const RETRY_DELAY_MS = 1500
function isTransientError(error: unknown): boolean {
const err = error as { code?: string; cause?: { code?: string; errors?: Array<{ code?: string }> }; errors?: Array<{ code?: string }> }
const code = err?.code ?? err?.cause?.code ?? err?.cause?.errors?.[0]?.code ?? err?.errors?.[0]?.code
return code ? TRANSIENT_ERROR_CODES.includes(code) : false
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const body = await req.json() const body = await req.json()
@ -12,12 +26,13 @@ export async function POST(req: Request) {
// Clean domain: remove protocol, trailing slashes, and trim whitespace // Clean domain: remove protocol, trailing slashes, and trim whitespace
const cleanDomain = domain.trim().replace(/^https?:\/\//, "").replace(/\/$/, "").trim() const cleanDomain = domain.trim().replace(/^https?:\/\//, "").replace(/\/$/, "").trim()
// Create an AbortController for timeout let lastError: unknown = null
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
const controller = new AbortController() const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000) const timeoutId = setTimeout(() => controller.abort(), 10000)
try { try {
// Shopify Shop API endpoint
const response = await fetch(`https://${cleanDomain}/admin/api/2024-01/shop.json`, { const response = await fetch(`https://${cleanDomain}/admin/api/2024-01/shop.json`, {
method: "GET", method: "GET",
headers: { headers: {
@ -42,15 +57,25 @@ export async function POST(req: Request) {
message: "Validation successful!", message: "Validation successful!",
shop: data.shop shop: data.shop
}) })
} catch (fetchError: unknown) { } catch (fetchError: unknown) {
clearTimeout(timeoutId) clearTimeout(timeoutId)
lastError = fetchError
if (fetchError instanceof Error && fetchError.name === "AbortError") { if (fetchError instanceof Error && fetchError.name === "AbortError") {
return NextResponse.json({ message: "Connection to Shopify timed out. Please check the domain." }, { status: 408 }) return NextResponse.json({ message: "Connection to Shopify timed out. Please check the domain." }, { status: 408 })
} }
throw fetchError
if (isTransientError(fetchError) && attempt < MAX_RETRIES) {
console.warn(`Shopify validation attempt ${attempt}/${MAX_RETRIES} failed (transient), retrying...`, fetchError)
await sleep(RETRY_DELAY_MS)
continue
} }
throw fetchError
}
}
throw lastError
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error" const message = error instanceof Error ? error.message : "Unknown error"
console.error("Shopify validation error:", error) console.error("Shopify validation error:", error)

View File

@ -1,5 +1,11 @@
import nodemailer from "nodemailer" import nodemailer from "nodemailer"
import { logger } from "@/lib/logger" import { logger } from "@/lib/logger"
import { getBaseUrl } from "@/lib/utils"
/** Returns the absolute URL for the JUNOHUB logo (for use in email templates) */
export function getEmailLogoUrl(): string {
return `${getBaseUrl()}/icon.png`
}
// Create reusable transporter (only if SMTP is configured) // Create reusable transporter (only if SMTP is configured)
function getTransporter() { function getTransporter() {
@ -80,11 +86,7 @@ export function generateInvitationEmailHtml(
<td style="padding: 40px 40px 30px; background: linear-gradient(135deg, rgba(79, 70, 229, 0.2) 0%, rgba(99, 102, 241, 0.1) 100%); border-bottom: 1px solid rgba(255, 255, 255, 0.1);"> <td style="padding: 40px 40px 30px; background: linear-gradient(135deg, rgba(79, 70, 229, 0.2) 0%, rgba(99, 102, 241, 0.1) 100%); border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="text-align: center;"> <div style="text-align: center;">
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 16px;"> <div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 16px;">
<div style="width: 48px; height: 48px; background-color: rgba(79, 70, 229, 0.4); border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.2); display: flex; align-items: center; justify-content: center;"> <img src="${getEmailLogoUrl()}" alt="JUNOHUB" width="48" height="48" style="display: block; border-radius: 12px; object-fit: contain;" />
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: block;">
<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" fill="white"/>
</svg>
</div>
<h1 style="margin: 0; font-size: 28px; font-weight: 800; color: #ffffff; letter-spacing: -0.5px;">JUNOHUB</h1> <h1 style="margin: 0; font-size: 28px; font-weight: 800; color: #ffffff; letter-spacing: -0.5px;">JUNOHUB</h1>
</div> </div>
</div> </div>