feat(email): add Resend integration with password reset and email verification
Install resend package. Add convex/lib/resend.ts for sending emails via Resend API. Update convex/auth.ts to enable requireEmailVerification, sendVerificationEmail, and sendResetPassword handlers. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
45368fb9c5
commit
d6da6e6193
4 changed files with 105 additions and 1 deletions
|
|
@ -8,6 +8,7 @@ import { DataModel } from './_generated/dataModel';
|
|||
import { query } from './_generated/server';
|
||||
import authConfig from './auth.config';
|
||||
import authSchema from './betterAuth/schema';
|
||||
import { sendEmail } from './lib/resend';
|
||||
|
||||
const siteUrl = process.env.SITE_URL!;
|
||||
|
||||
|
|
@ -31,7 +32,24 @@ export const createAuthOptions = (
|
|||
maxPasswordLength: 128,
|
||||
autoSignIn: true,
|
||||
enabled: true,
|
||||
requireEmailVerification: false,
|
||||
requireEmailVerification: true,
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
void sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
html: `<p>Click <a href="${url}">here</a> to reset your password.</p>`,
|
||||
});
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
void sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Verify your email',
|
||||
html: `<p>Click <a href="${url}">here</a> to verify your email address.</p>`,
|
||||
});
|
||||
},
|
||||
},
|
||||
plugins: [convex({ authConfig }), haveIBeenPwned()],
|
||||
};
|
||||
|
|
|
|||
31
convex/lib/resend.ts
Normal file
31
convex/lib/resend.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const RESEND_API_KEY = process.env.RESEND_API_KEY!;
|
||||
const RESEND_FROM_EMAIL = process.env.RESEND_FROM_EMAIL!;
|
||||
|
||||
interface SendEmailOptions {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export async function sendEmail({ to, subject, html }: SendEmailOptions) {
|
||||
const response = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${RESEND_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: RESEND_FROM_EMAIL,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Failed to send email: ${error}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-hook-form": "^7.72.0",
|
||||
"resend": "^6.12.2",
|
||||
"shadcn": "^4.1.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ importers:
|
|||
react-hook-form:
|
||||
specifier: ^7.72.0
|
||||
version: 7.72.0(react@19.2.4)
|
||||
resend:
|
||||
specifier: ^6.12.2
|
||||
version: 6.12.2
|
||||
shadcn:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1(@types/node@20.19.37)(typescript@5.9.3)
|
||||
|
|
@ -1798,6 +1801,9 @@ packages:
|
|||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@stablelib/base64@1.0.1':
|
||||
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
|
|
@ -3170,6 +3176,9 @@ packages:
|
|||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-sha256@1.3.0:
|
||||
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
|
|
@ -4167,6 +4176,9 @@ packages:
|
|||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
postal-mime@2.7.4:
|
||||
resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==}
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -4353,6 +4365,15 @@ packages:
|
|||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resend@6.12.2:
|
||||
resolution: {integrity: sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
'@react-email/render': '*'
|
||||
peerDependenciesMeta:
|
||||
'@react-email/render':
|
||||
optional: true
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -4523,6 +4544,9 @@ packages:
|
|||
stable-hash@0.0.5:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
standardwebhooks@1.0.0:
|
||||
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -4625,6 +4649,9 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
svix@1.90.0:
|
||||
resolution: {integrity: sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==}
|
||||
|
||||
tagged-tag@1.0.0:
|
||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||
engines: {node: '>=20'}
|
||||
|
|
@ -4812,6 +4839,10 @@ packages:
|
|||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
||||
validate-npm-package-name@7.0.2:
|
||||
resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
|
@ -6652,6 +6683,8 @@ snapshots:
|
|||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@stablelib/base64@1.0.1': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
|
@ -7995,6 +8028,8 @@ snapshots:
|
|||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-sha256@1.3.0: {}
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fastq@1.20.1:
|
||||
|
|
@ -8925,6 +8960,8 @@ snapshots:
|
|||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postal-mime@2.7.4: {}
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
|
|
@ -9174,6 +9211,11 @@ snapshots:
|
|||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resend@6.12.2:
|
||||
dependencies:
|
||||
postal-mime: 2.7.4
|
||||
svix: 1.90.0
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
|
@ -9438,6 +9480,11 @@ snapshots:
|
|||
|
||||
stable-hash@0.0.5: {}
|
||||
|
||||
standardwebhooks@1.0.0:
|
||||
dependencies:
|
||||
'@stablelib/base64': 1.0.1
|
||||
fast-sha256: 1.3.0
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
stdin-discarder@0.2.2: {}
|
||||
|
|
@ -9552,6 +9599,11 @@ snapshots:
|
|||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svix@1.90.0:
|
||||
dependencies:
|
||||
standardwebhooks: 1.0.0
|
||||
uuid: 10.0.0
|
||||
|
||||
tagged-tag@1.0.0: {}
|
||||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
|
@ -9772,6 +9824,8 @@ snapshots:
|
|||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
validate-npm-package-name@7.0.2: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
|
|
|||
Loading…
Reference in a new issue