From e433cea908c5cc09e00db6f9885f1db1c4236af0 Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Wed, 15 Apr 2026 03:30:44 +0000 Subject: [PATCH] feat(auth): enable email verification with Resend Co-Authored-By: Paperclip --- .env.example | 4 +++ package-lock.json | 75 ++++++++++++++++++++++++++++++++++++++++++++++- package.json | 7 +++-- src/auth.ts | 19 +++++++++++- 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 6e16447..f264af4 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,7 @@ DATABASE_URL=postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch # Port the auth service listens on PORT=3001 + +# Resend email provider for transactional email +RESEND_API_KEY=re_your_api_key_here +FROM_EMAIL=CartSnitch diff --git a/package-lock.json b/package-lock.json index 0051e96..ce0c339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "bcrypt": "^6.0.0", "better-auth": "^1.2.0", - "pg": "^8.13.0" + "pg": "^8.13.0", + "resend": "^6.11.0" }, "devDependencies": { "@types/bcrypt": "^6.0.0", @@ -633,6 +634,12 @@ "node": ">=14" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -858,6 +865,12 @@ "@esbuild/win32-x64": "0.27.4" } }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1028,6 +1041,12 @@ "split2": "^4.1.0" } }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1067,6 +1086,27 @@ "node": ">=0.10.0" } }, + "node_modules/resend": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.11.0.tgz", + "integrity": "sha512-S9gxOccfwc+E6Cr3q28Gu8NkiIjYlYPlj9rqk4zkIuzlEoh8sWu/IvJSg7U7t+o3g0Ov2IOCzcneUaCi/M/WdQ==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "svix": "1.90.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1098,6 +1138,26 @@ "node": ">= 10.x" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/svix": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.90.0.tgz", + "integrity": "sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1139,6 +1199,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index c4dcf1f..9eef257 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,16 @@ "generate": "npx @better-auth/cli generate" }, "dependencies": { + "bcrypt": "^6.0.0", "better-auth": "^1.2.0", "pg": "^8.13.0", - "bcrypt": "^6.0.0" + "resend": "^6.11.0" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/node": "^22.0.0", "@types/pg": "^8.11.0", - "@types/bcrypt": "^6.0.0", "tsx": "^4.19.0", "typescript": "^5.7.0" } -} +} \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index c882aac..95bbe2c 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,6 +1,7 @@ import { betterAuth } from "better-auth"; import bcrypt from "bcrypt"; import pg from "pg"; +import { Resend } from "resend"; const { Pool } = pg; @@ -21,6 +22,9 @@ export const pool = new Pool({ connectionString: databaseUrl ?? "postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch", }); +const resend = new Resend(process.env.RESEND_API_KEY); +const fromEmail = process.env.FROM_EMAIL || "CartSnitch "; + export const auth = betterAuth({ database: pool, basePath: "/auth", @@ -41,6 +45,19 @@ export const auth = betterAuth({ }, }, + emailVerification: { + sendOnSignUp: true, + autoSignInAfterVerification: true, + sendVerificationEmail: async ({ user, url }) => { + await resend.emails.send({ + from: fromEmail, + to: user.email, + subject: "Verify your CartSnitch email", + html: `

Hi ${user.name || ""},

Click the link below to verify your email address:

Verify Email

This link expires in 1 hour.

— CartSnitch

`, + }); + }, + }, + session: { modelName: "sessions", fields: { @@ -103,4 +120,4 @@ export const auth = betterAuth({ "https://cartsnitch.dev.farh.net", "https://cartsnitch.uat.farh.net", ], -}); +}); \ No newline at end of file