From 81ce92319b94dde13db424cd312075c163f2cd3b Mon Sep 17 00:00:00 2001 From: Mayank Saini Date: Thu, 9 Apr 2026 19:43:44 +0530 Subject: [PATCH 1/7] feat: complete email verification and SEO setup --- next-sitemap.config.js | 43 +++ package-lock.json | 305 ++++++++++++++++++ package.json | 5 + prisma/schema.prisma | 5 +- public/manifest.json | 13 + src/app/(auth)/check-email/page.tsx | 51 +++ src/app/(auth)/resend-verification/page.tsx | 101 ++++++ src/app/(auth)/verify-email/page.tsx | 92 ++++++ src/app/api/auth/custom-signup/route.ts | 87 +++++ src/app/api/auth/resend-verification/route.ts | 45 +++ src/app/api/auth/verify-email/route.ts | 58 ++++ src/app/globals.css | 124 +++++-- src/app/layout.tsx | 81 ++++- src/app/opengraph-image.tsx | 60 ++++ src/components/entity-components.tsx | 2 +- src/components/structured-data.tsx | 76 +++++ src/features/auth/components/login-form.tsx | 32 +- .../auth/components/register-form.tsx | 33 +- src/inngest/functions/schedule-poller.ts | 2 +- src/lib/auth.ts | 3 +- src/lib/email-verification.ts | 199 ++++++++++++ 21 files changed, 1356 insertions(+), 61 deletions(-) create mode 100644 next-sitemap.config.js create mode 100644 public/manifest.json create mode 100644 src/app/(auth)/check-email/page.tsx create mode 100644 src/app/(auth)/resend-verification/page.tsx create mode 100644 src/app/(auth)/verify-email/page.tsx create mode 100644 src/app/api/auth/custom-signup/route.ts create mode 100644 src/app/api/auth/resend-verification/route.ts create mode 100644 src/app/api/auth/verify-email/route.ts create mode 100644 src/app/opengraph-image.tsx create mode 100644 src/components/structured-data.tsx create mode 100644 src/lib/email-verification.ts diff --git a/next-sitemap.config.js b/next-sitemap.config.js new file mode 100644 index 0000000..82f555c --- /dev/null +++ b/next-sitemap.config.js @@ -0,0 +1,43 @@ +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl: "https://nodebase.tech", + generateRobotsTxt: true, + generateIndexSitemap: false, + changefreq: "weekly", + priority: 0.7, + sitemapSize: 5000, + exclude: [ + "/workflows/*", + "/editor/*", + "/settings/*", + "/api/*", + "/verify-email", + "/check-email", + "/resend-verification", + ], + additionalPaths: async (config) => [ + await config.transform(config, "/"), + await config.transform(config, "/pricing"), + await config.transform(config, "/blog"), + await config.transform(config, "/integrations"), + await config.transform(config, "/login"), + await config.transform(config, "/signup"), + ], + robotsTxtOptions: { + policies: [ + { + userAgent: "*", + allow: "/", + disallow: ["/workflows/", "/editor/", "/settings/", "/api/"], + }, + { + userAgent: "Googlebot", + allow: "/", + disallow: ["/workflows/", "/editor/", "/settings/"], + }, + ], + additionalSitemaps: [ + "https://nodebase.tech/sitemap.xml", + ], + }, +} diff --git a/package-lock.json b/package-lock.json index db75485..6c76253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "@vercel/analytics": "^1.6.1", "@xyflow/react": "^12.9.2", "ai": "^6.0.45", + "bcryptjs": "^3.0.3", "better-auth": "^1.2.7", "class-variance-authority": "^0.7.1", "client-only": "^0.0.1", @@ -77,6 +78,7 @@ "lucide-react": "^0.546.0", "motion": "^12.29.0", "next": "^15.5.13", + "next-sitemap": "^4.2.3", "next-themes": "^0.4.6", "node-fetch": "^2.7.0", "nodemailer": "^7.0.11", @@ -91,6 +93,7 @@ "react-hook-form": "^7.65.0", "react-resizable-panels": "^3.0.6", "recharts": "^2.15.4", + "schema-dts": "^2.0.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "stripe": "^20.4.1", @@ -105,6 +108,7 @@ "devDependencies": { "@biomejs/biome": "2.2.0", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/node-fetch": "^2.6.13", "@types/nodemailer": "^7.0.11", @@ -1230,6 +1234,12 @@ "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@corex/deepmerge": { + "version": "4.0.43", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz", + "integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==", + "license": "MIT" + }, "node_modules/@date-fns/tz": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", @@ -2686,6 +2696,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -7841,6 +7886,13 @@ "integrity": "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==", "license": "MIT" }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bunyan": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", @@ -8889,6 +8941,15 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/better-auth": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.5.5.tgz", @@ -9051,6 +9112,18 @@ "node": "18 || 20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -10126,6 +10199,22 @@ "node": ">=6.0.0" } }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-sha256": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", @@ -10184,6 +10273,15 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -10231,6 +10329,18 @@ "dev": true, "license": "MIT" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10500,6 +10610,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -10826,6 +10948,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -10835,6 +10966,27 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -11465,6 +11617,15 @@ "license": "MIT", "peer": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/meshoptimizer": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", @@ -11472,6 +11633,31 @@ "dev": true, "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -11761,6 +11947,39 @@ } } }, + "node_modules/next-sitemap": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz", + "integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==", + "funding": [ + { + "url": "https://github.com/iamvishnusankar/next-sitemap.git" + } + ], + "license": "MIT", + "dependencies": { + "@corex/deepmerge": "^4.0.43", + "@next/env": "^13.4.3", + "fast-glob": "^3.2.12", + "minimist": "^1.2.8" + }, + "bin": { + "next-sitemap": "bin/next-sitemap.mjs", + "next-sitemap-cjs": "bin/next-sitemap.cjs" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "next": "*" + } + }, + "node_modules/next-sitemap/node_modules/@next/env": { + "version": "13.5.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz", + "integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==", + "license": "MIT" + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -12396,6 +12615,26 @@ ], "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/random-word-slugs": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/random-word-slugs/-/random-word-slugs-0.1.7.tgz", @@ -12791,6 +13030,16 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.9", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", @@ -12875,12 +13124,56 @@ "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", "license": "MIT" }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/schema-dts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-2.0.0.tgz", + "integrity": "sha512-t7NoCy3Rn5GHGx6p7s1qIYK/AeIb8ZxJNR9WUNFkwMv2CiiGZBmqqYWc2FlZVm5ZbiHMY4OvBWhj7QtyrFO2Jw==", + "license": "Apache-2.0", + "dependencies": { + "schema-dts-lib": "^1.0.0" + } + }, + "node_modules/schema-dts-lib": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-dts-lib/-/schema-dts-lib-1.0.0.tgz", + "integrity": "sha512-9MEO5vpQH9JdBioUupqluzxSYxPLjhmqRUudk15adUl/ypnRsM2/M1kN3AmVJQeG7nZqcL68H8JlGqQQT6vy9A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + } + }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -13395,6 +13688,18 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", diff --git a/package.json b/package.json index 254fa74..8979489 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev --turbopack", "build": "prisma generate && next build --turbopack", + "postbuild": "next-sitemap", "start": "next start", "lint": "biome check", "format": "biome format --write", @@ -68,6 +69,7 @@ "@vercel/analytics": "^1.6.1", "@xyflow/react": "^12.9.2", "ai": "^6.0.45", + "bcryptjs": "^3.0.3", "better-auth": "^1.2.7", "class-variance-authority": "^0.7.1", "client-only": "^0.0.1", @@ -86,6 +88,7 @@ "lucide-react": "^0.546.0", "motion": "^12.29.0", "next": "^15.5.13", + "next-sitemap": "^4.2.3", "next-themes": "^0.4.6", "node-fetch": "^2.7.0", "nodemailer": "^7.0.11", @@ -100,6 +103,7 @@ "react-hook-form": "^7.65.0", "react-resizable-panels": "^3.0.6", "recharts": "^2.15.4", + "schema-dts": "^2.0.0", "server-only": "^0.0.1", "sonner": "^2.0.7", "stripe": "^20.4.1", @@ -114,6 +118,7 @@ "devDependencies": { "@biomejs/biome": "2.2.0", "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/node-fetch": "^2.6.13", "@types/nodemailer": "^7.0.11", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8012a75..58c3d62 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,7 +19,10 @@ model User { id String @id name String email String - emailVerified Boolean @default(false) + emailVerified Boolean @default(false) + emailVerifyToken String? @unique + emailVerifyExpiry DateTime? + emailVerifyAttempts Int @default(0) image String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..b4f442a --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "Nodebase", + "short_name": "Nodebase", + "description": "Workflow automation for India", + "start_url": "/", + "display": "standalone", + "background_color": "#111111", + "theme_color": "#F97316", + "icons": [ + { "src": "/favicon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/favicon-512.png", "sizes": "512x512", "type": "image/png" } + ] +} diff --git a/src/app/(auth)/check-email/page.tsx b/src/app/(auth)/check-email/page.tsx new file mode 100644 index 0000000..8a36026 --- /dev/null +++ b/src/app/(auth)/check-email/page.tsx @@ -0,0 +1,51 @@ +"use client" + +import { useSearchParams } from "next/navigation" +import Link from "next/link" +import { Suspense } from "react" +import { MailCheck } from "lucide-react" + +function CheckEmailContent() { + const searchParams = useSearchParams() + const email = searchParams.get("email") + + return ( +
+
+
+
+ +
+

+ Check your inbox +

+

+ We sent a verification link to
+ {email || "your email address"} +

+
+

+ Didn't receive it? Check your spam folder or{" "} + + request a new link + . +

+
+
+ + Return to login + +
+
+
+
+ ) +} + +export default function CheckEmailPage() { + return ( + + + + ) +} diff --git a/src/app/(auth)/resend-verification/page.tsx b/src/app/(auth)/resend-verification/page.tsx new file mode 100644 index 0000000..555eb50 --- /dev/null +++ b/src/app/(auth)/resend-verification/page.tsx @@ -0,0 +1,101 @@ +"use client" + +import { useState } from "react" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { toast } from "sonner" +import Link from "next/link" + +const resendSchema = z.object({ + email: z.string().email("Please enter a valid email address"), +}) + +export default function ResendVerificationPage() { + const [success, setSuccess] = useState(false) + + const form = useForm>({ + resolver: zodResolver(resendSchema), + defaultValues: { email: "" }, + }) + + const onSubmit = async (values: z.infer) => { + try { + const res = await fetch("/api/auth/resend-verification", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), + }) + const data = await res.json() + + if (res.ok) { + setSuccess(true) + } else { + toast.error(data.error || "Failed to resend verification email") + } + } catch (err) { + toast.error("Network error. Please try again.") + } + } + + const isPending = form.formState.isSubmitting + + return ( +
+ + + Resend Verification + + Enter your email to receive a new verification link + + + + {success ? ( +
+
+

Email Sent

+

+ If an account exists for {form.getValues().email}, we've sent a new verification link. +

+ +
+ ) : ( +
+ + ( + + Email address + + + + + + )} + /> + + + + )} +
+
+
+ ) +} diff --git a/src/app/(auth)/verify-email/page.tsx b/src/app/(auth)/verify-email/page.tsx new file mode 100644 index 0000000..5422138 --- /dev/null +++ b/src/app/(auth)/verify-email/page.tsx @@ -0,0 +1,92 @@ +"use client" + +import { useSearchParams, useRouter } from "next/navigation" +import { useEffect, useState, Suspense } from "react" +import Link from "next/link" + +function VerifyEmailContent() { + const searchParams = useSearchParams() + const router = useRouter() + const token = searchParams.get("token") + + const [status, setStatus] = useState<"loading" | "success" | "error" | "expired">("loading") + const [message, setMessage] = useState("") + + useEffect(() => { + if (!token) { + setStatus("error") + setMessage("No verification token found. Check your email for the correct link.") + return + } + + // Call the verify API + fetch("/api/auth/verify-email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token }), + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + setStatus("success") + setMessage("Email verified! Redirecting to login...") + setTimeout(() => router.push("/login?verified=true"), 2000) + } else if (data.error === "TOKEN_EXPIRED") { + setStatus("expired") + setMessage("This verification link has expired. Request a new one below.") + } else { + setStatus("error") + setMessage(data.error || "Verification failed. The link may be invalid.") + } + }) + .catch(() => { + setStatus("error") + setMessage("Network error. Please try again.") + }) + }, [token, router]) + + return ( +
+
+ {status === "loading" && ( + <> +
+

Verifying your email...

+ + )} + {status === "success" && ( + <> +
+

Email Verified!

+

{message}

+ + )} + {(status === "error" || status === "expired") && ( + <> +
+

+ {status === "expired" ? "Link Expired" : "Verification Failed"} +

+

{message}

+ + Resend Verification Email + + + )} +
+
+ ) +} + +export default function VerifyEmailPage() { + return ( + + + + ) +} diff --git a/src/app/api/auth/custom-signup/route.ts b/src/app/api/auth/custom-signup/route.ts new file mode 100644 index 0000000..771c0b6 --- /dev/null +++ b/src/app/api/auth/custom-signup/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from "next/server" +import prisma from "@/lib/db" +import { generateVerifyToken, getTokenExpiry, sendVerificationEmail } from "@/lib/email-verification" +import { hash } from "bcryptjs" +import { createId } from "@paralleldrive/cuid2" + +export async function POST(req: NextRequest) { + try { + const body = await req.json() + const { email, password, name } = body + + if (!email || !password) { + return NextResponse.json({ error: "Missing email or password" }, { status: 400 }) + } + + // 1. Check if email already exists + const existing = await prisma.user.findUnique({ where: { email } }) + if (existing) { + if (existing.emailVerified) { + return NextResponse.json({ error: "An account with this email already exists." }, { status: 400 }) + } else { + // Resend verification to unverified account + const token = generateVerifyToken() + await prisma.user.update({ + where: { email }, + data: { + emailVerifyToken: token, + emailVerifyExpiry: getTokenExpiry(), + emailVerifyAttempts: 0, + }, + }) + try { + await sendVerificationEmail(email, existing.name || email, token) + } catch (emailError) { + console.error("Failed to send verification email:", emailError) + } + return NextResponse.json({ success: "VERIFICATION_SENT", email }) + } + } + + // 2. Hash password + const hashedPassword = await hash(password, 12) + + // 3. Generate token + const verifyToken = generateVerifyToken() + const verifyExpiry = getTokenExpiry() + + // 4. Create user — NOT verified yet + // Since we're partially bypassing better-auth for custom signup, we must create Account records for passwords manually or just rely on better-auth's adapter if we sign in email later. + // Actually better-auth natively stores password in `Account` model. + // Let's create the User first. + const user = await prisma.user.create({ + data: { + id: createId(), + email, + name: name || email.split("@")[0], + emailVerified: false, + emailVerifyToken: verifyToken, + emailVerifyExpiry: verifyExpiry, + }, + }) + + // Also create account for password login + await prisma.account.create({ + data: { + id: createId(), + userId: user.id, + accountId: email, + providerId: "credential", + password: hashedPassword, + } + }) + + // 5. Send verification email (non-blocking) + try { + await sendVerificationEmail(email, user.name || email, verifyToken) + } catch (emailError) { + console.error("Failed to send verification email:", emailError) + } + + // 6. DO NOT log them in — return success directive + return NextResponse.json({ success: "VERIFICATION_SENT", email }) + } catch (error) { + console.error("Signup error:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/src/app/api/auth/resend-verification/route.ts b/src/app/api/auth/resend-verification/route.ts new file mode 100644 index 0000000..3f2d8be --- /dev/null +++ b/src/app/api/auth/resend-verification/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server" +import prisma from "@/lib/db" +import { generateVerifyToken, getTokenExpiry, sendResendVerificationEmail } from "@/lib/email-verification" + +export async function POST(req: NextRequest) { + try { + const { email } = await req.json() as { email: string } + + const user = await prisma.user.findUnique({ where: { email } }) + + // Always return success to prevent email enumeration attacks + if (!user || user.emailVerified) { + return NextResponse.json({ success: true }) + } + + // Rate limit: max 3 resends per user + if (user.emailVerifyAttempts >= 3) { + return NextResponse.json( + { error: "Too many resend attempts. Please contact support." }, + { status: 429 } + ) + } + + const token = generateVerifyToken() + + await prisma.user.update({ + where: { id: user.id }, + data: { + emailVerifyToken: token, + emailVerifyExpiry: getTokenExpiry(), + emailVerifyAttempts: { increment: 1 }, + }, + }) + + await sendResendVerificationEmail(email, user.name || email, token) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Resend verification error:", error) + return NextResponse.json( + { error: "Server error. Please try again." }, + { status: 500 } + ) + } +} diff --git a/src/app/api/auth/verify-email/route.ts b/src/app/api/auth/verify-email/route.ts new file mode 100644 index 0000000..4e63ac8 --- /dev/null +++ b/src/app/api/auth/verify-email/route.ts @@ -0,0 +1,58 @@ +import { NextRequest, NextResponse } from "next/server" +import prisma from "@/lib/db" + +export async function POST(req: NextRequest) { + try { + const { token } = await req.json() as { token: string } + + if (!token || typeof token !== "string") { + return NextResponse.json( + { error: "Invalid token" }, + { status: 400 } + ) + } + + const user = await prisma.user.findUnique({ + where: { emailVerifyToken: token }, + }) + + if (!user) { + return NextResponse.json( + { error: "Invalid or already used verification link." }, + { status: 400 } + ) + } + + if (user.emailVerified) { + return NextResponse.json( + { success: true, message: "Already verified" } + ) + } + + if (!user.emailVerifyExpiry || user.emailVerifyExpiry < new Date()) { + return NextResponse.json( + { error: "TOKEN_EXPIRED" }, + { status: 400 } + ) + } + + // Mark as verified — clear the token + await prisma.user.update({ + where: { id: user.id }, + data: { + emailVerified: true, + emailVerifyToken: null, + emailVerifyExpiry: null, + emailVerifyAttempts: 0, + }, + }) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error("Email verification error:", error) + return NextResponse.json( + { error: "Server error. Please try again." }, + { status: 500 } + ) + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 7a2796f..8c09747 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -151,63 +151,84 @@ } .dark { - --background: oklch(0.2598 0.0306 262.6666); - --foreground: oklch(0.9219 0 0); - --card: oklch(0.3106 0.0301 268.6365); - --card-foreground: oklch(0.9219 0 0); - --popover: oklch(0.29 0.0249 268.3986); - --popover-foreground: oklch(0.9219 0 0); + /* Pure black base - clean, professional */ + --background: oklch(0.17 0 0); + --foreground: oklch(0.95 0 0); + --card: oklch(0.2 0 0); + --card-foreground: oklch(0.95 0 0); + --popover: oklch(0.18 0 0); + --popover-foreground: oklch(0.95 0 0); + + /* Brand (orange stays the same in dark mode) */ --primary: oklch(0.6397 0.172 36.4421); --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.3095 0.0266 266.7132); - --secondary-foreground: oklch(0.9219 0 0); - --muted: oklch(0.3095 0.0266 266.7132); - --muted-foreground: oklch(0.7155 0 0); - --accent: oklch(0.338 0.0589 267.5867); - --accent-foreground: oklch(0.8823 0.0571 254.1284); + + /* Surfaces */ + --secondary: oklch(0.24 0 0); + --secondary-foreground: oklch(0.95 0 0); + --muted: oklch(0.24 0 0); + --muted-foreground: oklch(0.65 0 0); + --accent: oklch(0.26 0 0); + --accent-foreground: oklch(0.95 0 0); + + /* Destructive */ --destructive: oklch(0.6368 0.2078 25.3313); - --border: oklch(0.3843 0.0301 269.7337); - --input: oklch(0.3843 0.0301 269.7337); + --destructive-foreground: oklch(1 0 0); + + /* Lines and inputs */ + --border: oklch(0.3 0 0); + --input: oklch(0.3 0 0); --ring: oklch(0.6397 0.172 36.4421); + + /* Charts */ --chart-1: oklch(0.7156 0.0605 248.6845); --chart-2: oklch(0.7693 0.0876 34.1875); --chart-3: oklch(0.5778 0.0759 254.1573); --chart-4: oklch(0.5016 0.0849 259.4902); --chart-5: oklch(0.425241 0.0952 264.0306); - --sidebar: oklch(0.31 0.0283 267.7408); - --sidebar-foreground: oklch(0.9219 0 0); + + /* Sidebar - slightly darker than main bg */ + --sidebar: oklch(0.14 0 0); + --sidebar-foreground: oklch(0.95 0 0); --sidebar-primary: oklch(0.6397 0.172 36.4421); --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(0.338 0.0589 267.5867); - --sidebar-accent-foreground: oklch(0.8823 0.0571 254.1284); - --sidebar-border: oklch(0.3843 0.0301 269.7337); + --sidebar-accent: oklch(0.22 0 0); + --sidebar-accent-foreground: oklch(0.95 0 0); + --sidebar-border: oklch(0.3 0 0); --sidebar-ring: oklch(0.6397 0.172 36.4421); - --destructive-foreground: oklch(1 0 0); + + /* Orange brand (same as :root) */ + --orange: #F97316; + --orange-dim: #EA6C0A; + --orange-glow: rgba(249,115,22,0.22); + --orange-subtle: rgba(249,115,22,0.09); + + /* Keep all utility vars */ --radius: 0.425rem; --font-sans: Inter, sans-serif; --font-serif: Source Serif 4, serif; --font-mono: JetBrains Mono, monospace; --shadow-color: hsl(0 0% 0%); - --shadow-opacity: 0.1; + --shadow-opacity: 0.15; --shadow-blur: 3px; --shadow-spread: 0px; --shadow-offset-x: 0px; --shadow-offset-y: 1px; --letter-spacing: 0em; --spacing: 0.25rem; - --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), - 0px 1px 2px -1px hsl(0 0% 0% / 0.1); - --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), - 0px 1px 2px -1px hsl(0 0% 0% / 0.1); - --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), - 0px 2px 4px -1px hsl(0 0% 0% / 0.1); - --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), - 0px 4px 6px -1px hsl(0 0% 0% / 0.1); - --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.1), - 0px 8px 10px -1px hsl(0 0% 0% / 0.1); - --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.25); + --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.15); + --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.15); + --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.2), + 0px 1px 2px -1px hsl(0 0% 0% / 0.2); + --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.2), + 0px 1px 2px -1px hsl(0 0% 0% / 0.2); + --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.2), + 0px 2px 4px -1px hsl(0 0% 0% / 0.2); + --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.2), + 0px 4px 6px -1px hsl(0 0% 0% / 0.2); + --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.2), + 0px 8px 10px -1px hsl(0 0% 0% / 0.2); + --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.35); } @layer base { @@ -297,4 +318,39 @@ .orange-dot-pulse { animation: orangeDot 1.8s ease-in-out infinite; +} + +/* Dark mode logo visibility */ +.dark .dark-invert { + filter: invert(1); +} + +.dark .dark-brightness { + filter: brightness(0) invert(1); +} + +/* Smooth scroll globally */ +html { + scroll-behavior: smooth; +} + +/* Dark mode marketing overrides */ +.dark .mkt-logo-chip { + color: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.07); + background-color: rgba(255, 255, 255, 0.03); +} +.dark .mkt-logo-chip:hover { + color: rgba(255, 255, 255, 0.8); + border-color: rgba(255, 255, 255, 0.18); + background-color: rgba(255, 255, 255, 0.06); +} + +.dark .mkt-feature-card { + border-color: rgba(255, 255, 255, 0.06); + background-color: rgba(255, 255, 255, 0.02); +} +.dark .mkt-feature-card:hover { + border-color: rgba(249, 115, 22, 0.35); + background-color: rgba(249, 115, 22, 0.06); } \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976bf62..d6b06cd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { TRPCReactProvider } from "@/trpc/client"; import { Toaster } from "@/components/ui/sonner"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import { Provider } from "jotai"; +import { SoftwareAppStructuredData, OrganizationStructuredData } from "@/components/structured-data"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; @@ -19,8 +20,79 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Nodebase", - description: "Nodebase - AI Powered Workflow Automation", + metadataBase: new URL("https://nodebase.tech"), + title: { + default: "Nodebase — Workflow Automation Built for India", + template: "%s | Nodebase", + }, + description: + "Automate your business workflows with India's most powerful automation platform. " + + "Native support for Razorpay, Cashfree, MSG91, Shiprocket, Google Sheets, and 100+ apps. " + + "The n8n and Zapier alternative built for Indian D2C, SaaS, and fintech companies.", + keywords: [ + "workflow automation India", + "n8n alternative India", + "Zapier alternative India", + "business automation India", + "no-code automation India", + "Razorpay automation", + "Cashfree integration", + "D2C automation India", + "SaaS automation India", + "nodebase", + "workflow builder India", + "automation platform India", + "Shiprocket automation", + "MSG91 automation", + "Indian payment automation", + ], + authors: [{ name: "Nodebase" }], + creator: "Nodebase", + publisher: "Nodebase", + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, + openGraph: { + type: "website", + locale: "en_IN", + url: "https://nodebase.tech", + siteName: "Nodebase", + title: "Nodebase — Workflow Automation Built for India", + description: + "Automate your business workflows with India's most powerful automation platform. " + + "Native support for Razorpay, Cashfree, MSG91, Shiprocket, and 100+ apps.", + images: [ + { + url: "./logos/logo.png", + width: 1200, + height: 630, + alt: "Nodebase — Workflow Automation Platform for India", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "Nodebase — Workflow Automation Built for India", + description: + "The n8n/Zapier alternative for Indian businesses. " + + "Native Razorpay, Cashfree, MSG91 integrations.", + images: ["/og-image.png"], + creator: "@nodebasetech", + }, + alternates: { + canonical: "https://nodebase.tech", + }, + verification: { + google: process.env.GOOGLE_SITE_VERIFICATION || "", + }, }; export default function RootLayout({ @@ -32,14 +104,15 @@ export default function RootLayout({ + + diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx new file mode 100644 index 0000000..744730d --- /dev/null +++ b/src/app/opengraph-image.tsx @@ -0,0 +1,60 @@ +import { ImageResponse } from "next/og" + +export const runtime = "edge" +export const size = { width: 1200, height: 630 } +export const contentType = "image/png" + +export default async function Image() { + return new ImageResponse( + ( +
+
+ + Nodebase + +
+

+ Workflow Automation +
+ Built for India +

+

+ Razorpay · Cashfree · MSG91 · Shiprocket · 100+ integrations +

+
+ ), + size + ) +} diff --git a/src/components/entity-components.tsx b/src/components/entity-components.tsx index 8239f57..4c7d82d 100644 --- a/src/components/entity-components.tsx +++ b/src/components/entity-components.tsx @@ -228,7 +228,7 @@ export const EmptyView = ({ onNew }: EmptyViewProps) => { return ( - + diff --git a/src/components/structured-data.tsx b/src/components/structured-data.tsx new file mode 100644 index 0000000..92fc1bd --- /dev/null +++ b/src/components/structured-data.tsx @@ -0,0 +1,76 @@ +export function SoftwareAppStructuredData() { + const data = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + "name": "Nodebase", + "applicationCategory": "BusinessApplication", + "operatingSystem": "Web", + "url": "https://nodebase.tech", + "description": + "Workflow automation platform built for Indian businesses. " + + "Native integrations with Razorpay, Cashfree, MSG91, Shiprocket, and 100+ services.", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "INR", + }, + "creator": { + "@type": "Organization", + "name": "Nodebase", + "url": "https://nodebase.tech", + }, + "featureList": [ + "Visual workflow builder", + "Razorpay integration", + "Cashfree integration", + "MSG91 SMS automation", + "Shiprocket automation", + "Google Sheets automation", + "PostgreSQL queries", + "HTTP API requests", + "Schedule-based triggers", + "Webhook triggers", + ], + "screenshot": "https://nodebase.tech/og-image.png", + "inLanguage": "en-IN", + "audience": { + "@type": "Audience", + "audienceType": "Indian businesses, D2C brands, SaaS companies, developers", + }, + } + + return ( +