diff --git a/bun.lock b/bun.lock index 7e94d4b..dfaf79c 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "better-auth": "^1.4.3", "lucide-svelte": "^0.556.0", "postcss": "^8.5.6", + "resend": "^6.5.2", "zod": "^4.1.13", }, "devDependencies": { @@ -275,6 +276,8 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="], + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.7", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q=="], @@ -511,6 +514,8 @@ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -527,6 +532,8 @@ "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], @@ -641,10 +648,16 @@ "proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="], + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "regexparam": ["regexparam@3.0.0", "", {}, "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q=="], + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "resend": ["resend@6.5.2", "", { "dependencies": { "svix": "1.76.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-Yl83UvS8sYsjgmF8dVbNPzlfpmb3DkLUk3VwsAbkaEFo9UMswpNuPGryHBXGk+Ta4uYMv5HmjVk3j9jmNkcEDg=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], @@ -683,6 +696,8 @@ "svelte-check": ["svelte-check@4.3.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw=="], + "svix": ["svix@1.76.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "@types/node": "^22.7.5", "es6-promise": "^4.2.8", "fast-sha256": "^1.3.0", "url-parse": "^1.5.10", "uuid": "^10.0.0" } }, "sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw=="], + "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -709,6 +724,10 @@ "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], diff --git a/docs/authentication.md b/docs/authentication.md index 6b166a7..86bebe6 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -301,22 +301,101 @@ Before deploying to production, ensure: ### Email Verification -Enable in `src/lib/server/auth.ts`: +Email verification and password reset are configured in `src/lib/server/auth.ts` using Resend as the email provider. -```typescript -export function createAuth(db: D1Database, secret: string, url: string) { - return betterAuth({ - // ... other config - emailAndPassword: { - enabled: true, - requireEmailVerification: true // Enable verification for production - } - // ... rest of config - }); -} -``` +#### Setup + +1. **Get a Resend API Key** + - Sign up at [resend.com](https://resend.com) + - Create an API key in your dashboard + - Verify your domain (or use their test domain for development) + +2. **Configure the Secret in Cloudflare** + + For **production**: + + ```bash + wrangler secret put RESEND_API_KEY --env production + ``` + + For **preview/staging**: + + ```bash + wrangler secret put RESEND_API_KEY --env preview + ``` + + For **local development**: + Add to `.dev.vars` (create if doesn't exist): + + ``` + RESEND_API_KEY=re_your_api_key_here + ``` + +3. **Email Configuration** + + The email functions are configured in `src/lib/server/auth.ts`: + + ```typescript + emailAndPassword: { + enabled: true, + requireEmailVerification: !!resendApiKey, // Auto-enabled when Resend is configured + + sendVerificationEmail: async ({ user, url }) => { + const resend = new Resend(resendApiKey); + await resend.emails.send({ + from: 'UHabit ', + to: user.email, + subject: 'Verify your email', + html: `...` + }); + }, + + sendResetPassword: async ({ user, url }) => { + const resend = new Resend(resendApiKey); + await resend.emails.send({ + from: 'UHabit ', + to: user.email, + subject: 'Reset your password', + html: `...` + }); + } + } + ``` + +#### Email Verification Flow + +1. User signs up +2. Better Auth creates unverified account +3. Verification email sent via Resend +4. User clicks link in email → `/api/auth/verify-email?token=...` +5. Better Auth verifies token and marks email as verified +6. User redirected to login or dashboard + +#### Password Reset Flow + +1. User clicks "Forgot Password" +2. Frontend calls `authClient.forgetPassword({ email })` +3. Better Auth generates reset token +4. Reset email sent via Resend +5. User clicks link → `/api/auth/reset-password?token=...` +6. User enters new password +7. Better Auth validates token and updates password + +#### Testing Locally + +Without a Resend API key: + +- Email verification is disabled +- Users can sign up and log in immediately +- Password reset won't work + +With Resend (development mode): + +- Use Resend's test domain or verify your own domain +- Emails will be sent normally +- Check Resend dashboard for sent emails -**Note**: Email verification is automatically disabled in dev mode. To test email verification, you'll need to configure an email provider and disable dev mode. +**Note**: Better Auth handles all the routing and logic automatically. You don't need to create custom pages unless you want to customize the UI. ### Custom User Fields diff --git a/package.json b/package.json index 9ef6875..a074b79 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "better-auth": "^1.4.3", "lucide-svelte": "^0.556.0", "postcss": "^8.5.6", + "resend": "^6.5.2", "zod": "^4.1.13" } } diff --git a/src/app.d.ts b/src/app.d.ts index 5f30290..44b6ac0 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -34,6 +34,8 @@ declare global { BETTER_AUTH_URL?: string; // Optional: auto-detects from request if not set DEV_MODE?: string; // Optional: set to "true" for dev mode RATE_LIMIT?: KVNamespace; // Optional: KV namespace for rate limiting + QUOTES_CACHE?: KVNamespace; // Optional: KV namespace for quotes caching + RESEND_API_KEY?: string; // Optional: Resend API key for sending emails }; context: { waitUntil(promise: Promise): void; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 77b1f63..131736c 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -22,6 +22,7 @@ export const handle: Handle = async ({ event, resolve }) => { const secret = event.platform?.env?.BETTER_AUTH_SECRET; const url = event.platform?.env?.BETTER_AUTH_URL || event.url.origin; const devModeEnv = event.platform?.env?.DEV_MODE === 'true'; + const resendApiKey = event.platform?.env?.RESEND_API_KEY; // Detect staging/preview environments const isStagingOrPreview = @@ -54,7 +55,7 @@ export const handle: Handle = async ({ event, resolve }) => { // Create auth instance and store in locals // The route handler at /api/auth/[...all] will use this - const auth = createAuth(db, secret, url, devMode); + const auth = createAuth(db, secret, url, devMode, resendApiKey); event.locals.auth = auth; // For non-auth routes, fetch session to populate locals.user diff --git a/src/lib/auth/client.ts b/src/lib/auth/client.ts index dcec0f9..84a8df9 100644 --- a/src/lib/auth/client.ts +++ b/src/lib/auth/client.ts @@ -40,6 +40,21 @@ export async function signOut() { return result.data; } +export async function forgetPassword(email: string) { + // Better Auth uses forgetPassword method from the email/password plugin + // @ts-ignore - Type definitions may be incomplete for this method + const result = await authClient.forgetPassword({ + email, + redirectTo: '/reset-password' + }); + + if (result.error) { + throw new Error(result.error.message || 'Failed to send reset email'); + } + + return result.data; +} + export async function getSession() { const result = await authClient.getSession(); return result.data; diff --git a/src/lib/routes.ts b/src/lib/routes.ts index 5a4b685..28897a9 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -7,6 +7,9 @@ export type HabitType = 'progressive' | 'single'; export const routes = { login: '/login', + register: '/register', + forgotPassword: '/forgot-password', + resetPassword: '/reset-password', overview: '/overview', habits: { diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index a12908d..af6a7dd 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -2,10 +2,18 @@ import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { haveIBeenPwned } from 'better-auth/plugins'; import { APIError } from 'better-auth/api'; +import { Resend } from 'resend'; import { getDB } from './db'; import * as schema from './db/schema'; +import { getVerificationEmailTemplate, getPasswordResetEmailTemplate } from './email-templates'; -export function createAuth(db: D1Database, secret: string, url: string, devMode = false) { +export function createAuth( + db: D1Database, + secret: string, + url: string, + devMode = false, + resendApiKey?: string +) { // Detect if URL is a staging/preview environment (dev mode allowed) // Preview: preview-123.uhabit.pages.dev // Staging: staging.uhabit.pages.dev @@ -63,18 +71,16 @@ export function createAuth(db: D1Database, secret: string, url: string, devMode ], emailAndPassword: { enabled: true, - // In dev mode, skip email verification for easier testing - requireEmailVerification: false, - // In production enable this: - // requireEmailVerification: !isDev + // Enable email verification only in production + requireEmailVerification: !!resendApiKey && !isDev, // Password requirements (enforced by Better Auth) - minPasswordLength: isDev ? 4 : 8, + minPasswordLength: isDev ? 1 : 8, // Note: Additional password validation happens client-side // via PasswordStrengthIndicator component // Lower bcrypt cost for Cloudflare Workers (10ms CPU limit) - // Default is 10, but that exceeds Workers CPU limits + // Default is 10, that exceeds Workers CPU limits password: { hash: async (password: string) => { const bcrypt = await import('bcryptjs'); @@ -84,7 +90,29 @@ export function createAuth(db: D1Database, secret: string, url: string, devMode const bcrypt = await import('bcryptjs'); return bcrypt.compare(data.password, data.hash); } - } + }, + + // Send verification and password reset emails + ...(resendApiKey && { + sendVerificationEmail: async ({ user, url }: { user: any; url: string }) => { + const resend = new Resend(resendApiKey); + await resend.emails.send({ + from: 'UHabit ', + to: user.email, + subject: 'Verify your email - UHabit', + html: getVerificationEmailTemplate(url, user.name) + }); + }, + sendResetPassword: async ({ user, url }: { user: any; url: string }) => { + const resend = new Resend(resendApiKey); + await resend.emails.send({ + from: 'UHabit ', + to: user.email, + subject: 'Reset your password - UHabit', + html: getPasswordResetEmailTemplate(url, user.name) + }); + } + }) }, session: { // Extended session in dev mode for convenience diff --git a/src/lib/server/email-templates.ts b/src/lib/server/email-templates.ts new file mode 100644 index 0000000..3ecfdfb --- /dev/null +++ b/src/lib/server/email-templates.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Load and populate an email template + */ +function loadTemplate(templateName: string, variables: Record): string { + const templatePath = join(process.cwd(), 'src/lib/server/email-templates', templateName); + let html = readFileSync(templatePath, 'utf-8'); + + // Replace all variables in the template + for (const [key, value] of Object.entries(variables)) { + html = html.replace(new RegExp(`{{${key}}}`, 'g'), value); + } + + return html; +} + +/** + * Get verification email HTML + */ +export function getVerificationEmailTemplate(verifyUrl: string, userName?: string): string { + return loadTemplate('verify-email.html', { + VERIFY_URL: verifyUrl, + USER_NAME: userName || 'there' + }); +} + +/** + * Get password reset email HTML + */ +export function getPasswordResetEmailTemplate(resetUrl: string, userName?: string): string { + return loadTemplate('reset-password.html', { + RESET_URL: resetUrl, + USER_NAME: userName || 'there' + }); +} diff --git a/src/lib/server/email-templates/reset-password.html b/src/lib/server/email-templates/reset-password.html new file mode 100644 index 0000000..7b1ab7a --- /dev/null +++ b/src/lib/server/email-templates/reset-password.html @@ -0,0 +1,249 @@ + + + + + + + Reset your password + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/src/lib/server/email-templates/verify-email.html b/src/lib/server/email-templates/verify-email.html new file mode 100644 index 0000000..c3bbfa8 --- /dev/null +++ b/src/lib/server/email-templates/verify-email.html @@ -0,0 +1,205 @@ + + + + + + + Verify your email + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/src/routes/api/auth/[...all]/+server.ts b/src/routes/api/auth/[...all]/+server.ts index 954551a..aefcbe5 100644 --- a/src/routes/api/auth/[...all]/+server.ts +++ b/src/routes/api/auth/[...all]/+server.ts @@ -73,8 +73,8 @@ export const POST: RequestHandler = async (event) => { } } - // Server-side password validation for registration - if (path.includes('/sign-up/email')) { + // Server-side password validation for registration (skip in dev/staging/preview) + if (path.includes('/sign-up/email') && !isStagingOrPreview) { try { const clonedRequest = event.request.clone(); const body = (await clonedRequest.json()) as { password?: string }; diff --git a/src/routes/forgot-password/+page.svelte b/src/routes/forgot-password/+page.svelte new file mode 100644 index 0000000..982b9fa --- /dev/null +++ b/src/routes/forgot-password/+page.svelte @@ -0,0 +1,9 @@ + + +
+
+ +
+
diff --git a/src/routes/forgot-password/ForgotPasswordForm.svelte b/src/routes/forgot-password/ForgotPasswordForm.svelte new file mode 100644 index 0000000..ec10fe6 --- /dev/null +++ b/src/routes/forgot-password/ForgotPasswordForm.svelte @@ -0,0 +1,117 @@ + + +
+
+

Forgot Password

+

+ Enter your email address and we'll send you a link to reset your password. +

+
+ + {#if errorMessage} +
+ {errorMessage} +
+ {/if} + + {#if successMessage} +
+ {successMessage} +
+ {/if} + +
+ + + {#if emailError} +

{emailError}

+ {/if} +
+ + + +

+ Remember your password? Sign in +

+
diff --git a/src/routes/login/LoginForm.svelte b/src/routes/login/LoginForm.svelte index 96ccc72..4d71efa 100644 --- a/src/routes/login/LoginForm.svelte +++ b/src/routes/login/LoginForm.svelte @@ -85,9 +85,14 @@
- +
+ + Forgot password? +
+ import ResetPasswordForm from './ResetPasswordForm.svelte'; + + +
+
+ +
+
diff --git a/src/routes/reset-password/ResetPasswordForm.svelte b/src/routes/reset-password/ResetPasswordForm.svelte new file mode 100644 index 0000000..c4f3cc0 --- /dev/null +++ b/src/routes/reset-password/ResetPasswordForm.svelte @@ -0,0 +1,141 @@ + + +
+
+

Reset Password

+

Enter your new password below.

+
+ + {#if errorMessage} +
+ {errorMessage} +
+ {/if} + + {#if successMessage} +
+ {successMessage} +
+ {/if} + + {#if token} +
+ + +
+ +
+ + +
+ + + {/if} + +

+ Remember your password? Sign in +

+