@amirulabu/create-recurring-rabbit-app 0.0.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/bin/index.js +2 -0
  2. package/dist/index.js +592 -0
  3. package/package.json +43 -0
  4. package/templates/default/.editorconfig +21 -0
  5. package/templates/default/.env.example +15 -0
  6. package/templates/default/.eslintrc.json +35 -0
  7. package/templates/default/.prettierrc.json +7 -0
  8. package/templates/default/README.md +346 -0
  9. package/templates/default/app.config.ts +20 -0
  10. package/templates/default/docs/adding-features.md +439 -0
  11. package/templates/default/docs/adr/001-use-sqlite-for-development-database.md +22 -0
  12. package/templates/default/docs/adr/002-use-tanstack-start-over-nextjs.md +22 -0
  13. package/templates/default/docs/adr/003-use-better-auth-over-nextauth.md +22 -0
  14. package/templates/default/docs/adr/004-use-drizzle-over-prisma.md +22 -0
  15. package/templates/default/docs/adr/005-use-trpc-for-api-layer.md +22 -0
  16. package/templates/default/docs/adr/006-use-tailwind-css-v4-with-shadcn-ui.md +22 -0
  17. package/templates/default/docs/architecture.md +241 -0
  18. package/templates/default/docs/database.md +376 -0
  19. package/templates/default/docs/deployment.md +435 -0
  20. package/templates/default/docs/troubleshooting.md +668 -0
  21. package/templates/default/drizzle/migrations/0001_initial_schema.sql +39 -0
  22. package/templates/default/drizzle/migrations/meta/0001_snapshot.json +225 -0
  23. package/templates/default/drizzle/migrations/meta/_journal.json +12 -0
  24. package/templates/default/drizzle.config.ts +10 -0
  25. package/templates/default/lighthouserc.json +78 -0
  26. package/templates/default/src/app/__root.tsx +32 -0
  27. package/templates/default/src/app/api/auth/$.ts +15 -0
  28. package/templates/default/src/app/api/trpc.server.ts +12 -0
  29. package/templates/default/src/app/auth/forgot-password.tsx +107 -0
  30. package/templates/default/src/app/auth/login.tsx +34 -0
  31. package/templates/default/src/app/auth/register.tsx +34 -0
  32. package/templates/default/src/app/auth/reset-password.tsx +171 -0
  33. package/templates/default/src/app/auth/verify-email.tsx +111 -0
  34. package/templates/default/src/app/dashboard/index.tsx +122 -0
  35. package/templates/default/src/app/dashboard/settings.tsx +161 -0
  36. package/templates/default/src/app/globals.css +55 -0
  37. package/templates/default/src/app/index.tsx +83 -0
  38. package/templates/default/src/components/features/auth/login-form.tsx +172 -0
  39. package/templates/default/src/components/features/auth/register-form.tsx +202 -0
  40. package/templates/default/src/components/layout/dashboard-layout.tsx +27 -0
  41. package/templates/default/src/components/layout/header.tsx +29 -0
  42. package/templates/default/src/components/layout/sidebar.tsx +38 -0
  43. package/templates/default/src/components/ui/button.tsx +57 -0
  44. package/templates/default/src/components/ui/card.tsx +79 -0
  45. package/templates/default/src/components/ui/input.tsx +24 -0
  46. package/templates/default/src/lib/api.ts +42 -0
  47. package/templates/default/src/lib/auth.ts +292 -0
  48. package/templates/default/src/lib/email.ts +221 -0
  49. package/templates/default/src/lib/env.ts +119 -0
  50. package/templates/default/src/lib/hydration-timing.ts +289 -0
  51. package/templates/default/src/lib/monitoring.ts +336 -0
  52. package/templates/default/src/lib/utils.ts +6 -0
  53. package/templates/default/src/server/api/root.ts +10 -0
  54. package/templates/default/src/server/api/routers/dashboard.ts +37 -0
  55. package/templates/default/src/server/api/routers/user.ts +31 -0
  56. package/templates/default/src/server/api/trpc.ts +132 -0
  57. package/templates/default/src/server/auth/config.ts +241 -0
  58. package/templates/default/src/server/db/index.ts +153 -0
  59. package/templates/default/src/server/db/migrate.ts +125 -0
  60. package/templates/default/src/server/db/schema.ts +170 -0
  61. package/templates/default/src/server/db/seed.ts +130 -0
  62. package/templates/default/src/types/global.d.ts +25 -0
  63. package/templates/default/tailwind.config.js +46 -0
  64. package/templates/default/tsconfig.json +36 -0
@@ -0,0 +1,24 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
6
+
7
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
8
+ ({ className, type, ...props }, ref) => {
9
+ return (
10
+ <input
11
+ type={type}
12
+ className={cn(
13
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
14
+ className
15
+ )}
16
+ ref={ref}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+ )
22
+ Input.displayName = 'Input'
23
+
24
+ export { Input }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * tRPC client configuration module for @trpc/tanstack-react-query v11.
3
+ *
4
+ * This module sets up tRPC React client for type-safe API communication between
5
+ * frontend and backend. tRPC provides end-to-end type safety without requiring
6
+ * separate API contracts or code generation steps.
7
+ *
8
+ * Key patterns and conventions:
9
+ * - Use `useTRPC()` hook to get the tRPC proxy in components
10
+ * - Use `useQuery(trpc.procedure.queryOptions({ input }))` for queries
11
+ * - Use `useMutation(trpc.procedure.mutationOptions())` for mutations
12
+ * - Type definitions are inferred from backend AppRouter
13
+ * - Uses TanStack React Query under the hood for caching and deduplication
14
+ *
15
+ * @example
16
+ * // Query usage in a component
17
+ * function Dashboard() {
18
+ * const trpc = useTRPC()
19
+ * const { data: stats } = useQuery(trpc.dashboard.getStats.queryOptions())
20
+ * return <div>{stats?.totalUsers}</div>
21
+ * }
22
+ *
23
+ * @example
24
+ * // Mutation usage in a component
25
+ * function ProfileSettings() {
26
+ * const trpc = useTRPC()
27
+ * const { data: user } = useQuery(trpc.user.getProfile.queryOptions())
28
+ * const updateProfile = useMutation(trpc.user.updateProfile.mutationOptions())
29
+ *
30
+ * const handleSubmit = async () => {
31
+ * await updateProfile.mutateAsync({ name: 'New Name' })
32
+ * }
33
+ * return <form onSubmit={handleSubmit}>...</form>
34
+ * }
35
+ */
36
+
37
+ import { createTRPCContext } from '@trpc/tanstack-react-query'
38
+ import type { AppRouter } from '@/server/api/root'
39
+
40
+ export const { TRPCProvider, useTRPC, useTRPCClient } =
41
+ createTRPCContext<AppRouter>()
42
+ export type { AppRouter }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Client-Side Authentication Module
3
+ *
4
+ * This module provides the React hooks and client functions for authentication
5
+ * operations in the browser. It communicates with the server-side auth API
6
+ * to handle user sessions, sign in, sign out, and password recovery.
7
+ *
8
+ * KEY AUTHENTICATION FLOWS:
9
+ * 1. Sign In - Authenticate with credentials (email/password or OAuth)
10
+ * 2. Sign Up - Create new user account
11
+ * 3. Sign Out - Terminate current session
12
+ * 4. Session Management - Query and react to session state changes
13
+ * 5. Password Recovery - Request and complete password reset
14
+ *
15
+ * SECURITY CONSIDERATIONS:
16
+ * - All authentication requests go through the server (never directly to database)
17
+ * - Session tokens are stored in HTTP-only cookies (inaccessible to JavaScript)
18
+ * - Client only maintains session state, never raw credentials
19
+ * - Session state is reactive and automatically updates from server
20
+ * - SSR (Server-Side Rendering) safe: checks window object before accessing browser APIs
21
+ *
22
+ * SSR/SSG COMPATIBILITY:
23
+ * - Checks for window object before accessing browser-only APIs
24
+ * - Falls back to PUBLIC_APP_URL for server-side rendering
25
+ * - Default to localhost:3000 for development
26
+ */
27
+
28
+ import { createAuthClient } from 'better-auth/react'
29
+
30
+ /**
31
+ * Authentication client instance configured for the application.
32
+ *
33
+ * This client is the main entry point for all client-side authentication operations.
34
+ * It's initialized with the appropriate base URL depending on whether it's running
35
+ * in the browser or on the server (for SSR).
36
+ *
37
+ * BASE URL RESOLUTION:
38
+ * 1. Browser context: Uses window.location.origin (current domain)
39
+ * 2. Server context (SSR): Uses PUBLIC_APP_URL environment variable
40
+ * 3. Fallback: Uses localhost:3000 for local development
41
+ *
42
+ * SECURITY:
43
+ * - Base URL must match the server auth endpoint to prevent origin errors
44
+ * - In production, ensure PUBLIC_APP_URL is set correctly
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * import { authClient } from '@/lib/auth'
49
+ *
50
+ * // Sign in with email and password
51
+ * const result = await authClient.signIn.email({
52
+ * email: 'user@example.com',
53
+ * password: 'password123'
54
+ * })
55
+ * ```
56
+ */
57
+ export const authClient = createAuthClient({
58
+ /**
59
+ * Base URL for authentication API endpoints.
60
+ *
61
+ * Dynamically determined based on execution context:
62
+ * - Browser: Current origin (domain + protocol + port)
63
+ * - Server: PUBLIC_APP_URL environment variable
64
+ * - Fallback: http://localhost:3000
65
+ *
66
+ * SECURITY: Must match the actual auth server URL. Mismatched origins
67
+ * will cause CORS errors and prevent authentication from working.
68
+ */
69
+ baseURL:
70
+ typeof window !== 'undefined'
71
+ ? window.location.origin
72
+ : process.env.PUBLIC_APP_URL || 'http://localhost:3000',
73
+ })
74
+
75
+ /**
76
+ * Destructured authentication client methods for easier imports.
77
+ *
78
+ * These functions and hooks provide the primary authentication interface
79
+ * for React components. Each method is typed and ready to use throughout
80
+ * the application.
81
+ *
82
+ * EXPORTED METHODS:
83
+ * - signIn: General sign-in function (supports OAuth and other providers)
84
+ * - signUp: Create new user account
85
+ * - signOut: Terminate current user session
86
+ * - useSession: React hook for accessing session state
87
+ * - signInEmail: Sign in specifically with email/password
88
+ * - signUpEmail: Create account with email/password
89
+ * - forgotPassword: Request password reset email
90
+ * - resetPassword: Complete password reset with token
91
+ */
92
+
93
+ /**
94
+ * Sign in with credentials.
95
+ *
96
+ * Authenticates a user with email/password or OAuth providers.
97
+ *
98
+ * @param credentials - User credentials (email, password, provider)
99
+ * @returns Promise resolving to session data and user object
100
+ *
101
+ * SECURITY:
102
+ * - Credentials are sent over HTTPS in production
103
+ * - Server validates credentials before creating session
104
+ * - Session token stored in HTTP-only cookie after success
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * const { data, error } = await signIn({
109
+ * email: 'user@example.com',
110
+ * password: 'password123'
111
+ * })
112
+ * ```
113
+ */
114
+ export const { signIn } = authClient
115
+
116
+ /**
117
+ * Sign up new user account.
118
+ *
119
+ * Creates a new user account with the provided credentials.
120
+ * Email verification is required before sign in is allowed.
121
+ *
122
+ * @param credentials - Registration credentials (email, password)
123
+ * @returns Promise resolving to created user data
124
+ *
125
+ * SECURITY:
126
+ * - Password is hashed on the server before storage
127
+ * - Email verification required prevents spam accounts
128
+ * - Same password rules apply as sign-in
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * const { data, error } = await signUp({
133
+ * email: 'user@example.com',
134
+ * password: 'securePassword123'
135
+ * })
136
+ * ```
137
+ */
138
+ export const { signUp } = authClient
139
+
140
+ /**
141
+ * Sign out current user.
142
+ *
143
+ * Terminates the current user session and clears authentication state.
144
+ *
145
+ * @returns Promise resolving when sign out is complete
146
+ *
147
+ * SECURITY:
148
+ * - Invalidates session token on server
149
+ * - Clears session cookie
150
+ * - All subsequent requests will be unauthenticated
151
+ *
152
+ * @example
153
+ * ```tsx
154
+ * await signOut()
155
+ * // User is now signed out
156
+ * ```
157
+ */
158
+ export const { signOut } = authClient
159
+
160
+ /**
161
+ * React hook for accessing session state.
162
+ *
163
+ * Provides reactive access to the current user's session and user data.
164
+ * Automatically updates when session changes (sign in, sign out, expiry).
165
+ *
166
+ * @returns Object containing:
167
+ * - data: Current session and user data (null if not authenticated)
168
+ * - error: Error object if session check failed
169
+ * - isLoading: Boolean indicating if session is being fetched
170
+ * - isPending: Boolean indicating if session is being refreshed
171
+ *
172
+ * SECURITY:
173
+ * - Only exposes non-sensitive user data
174
+ * - Automatically handles session expiry
175
+ * - Safe to call from any component
176
+ *
177
+ * @example
178
+ * ```tsx
179
+ * function UserProfile() {
180
+ * const { data: session, isLoading } = useSession()
181
+ *
182
+ * if (isLoading) return <LoadingSpinner />
183
+ * if (!session) return <SignInButton />
184
+ *
185
+ * return <div>Welcome, {session.user.name}</div>
186
+ * }
187
+ * ```
188
+ */
189
+ export const { useSession } = authClient
190
+
191
+ /**
192
+ * Sign in with email and password.
193
+ *
194
+ * In better-auth v1.0.0+, signIn is an object with provider-specific
195
+ * methods. Use signIn.email() for email/password authentication.
196
+ *
197
+ * @param credentials - Object containing email and password
198
+ * @returns Promise resolving to session data
199
+ *
200
+ * SECURITY:
201
+ * - Validates email format and password strength on server
202
+ * - Rate limiting prevents brute force attacks
203
+ * - Failed attempts are logged for security monitoring
204
+ *
205
+ * @example
206
+ * ```tsx
207
+ * const { data, error } = await authClient.signIn.email({
208
+ * email: 'user@example.com',
209
+ * password: 'password123'
210
+ * })
211
+ * ```
212
+ */
213
+ // Note: signIn.email() is the method for email/password auth
214
+ // Example usage: await authClient.signIn.email({ email, password })
215
+
216
+ /**
217
+ * Sign up with email and password.
218
+ *
219
+ * In better-auth v1.0.0+, signUp is an object with provider-specific
220
+ * methods. Use signUp.email() for email/password registration.
221
+ *
222
+ * @param credentials - Object containing email, password, and name
223
+ * @returns Promise resolving to created user data
224
+ *
225
+ * SECURITY:
226
+ * - Password must meet strength requirements
227
+ * - Email is sent to verify ownership
228
+ * - User cannot sign in until email is verified
229
+ *
230
+ * @example
231
+ * ```tsx
232
+ * const { data, error } = await authClient.signUp.email({
233
+ * email: 'user@example.com',
234
+ * password: 'securePassword123',
235
+ * name: 'John Doe'
236
+ * })
237
+ * // Email verification required before sign-in
238
+ * ```
239
+ */
240
+ // Note: signUp.email() is the method for email/password registration
241
+ // Example usage: await authClient.signUp.email({ email, password, name })
242
+
243
+ /**
244
+ * Request password reset email.
245
+ *
246
+ * Initiates password reset flow by sending a reset link to user's email.
247
+ * The link contains a single-use token that expires after a set time.
248
+ *
249
+ * @param email - User's registered email address
250
+ * @returns Promise resolving when email is sent
251
+ *
252
+ * SECURITY:
253
+ * - Only reveals if email exists in timing-attack-resistant way
254
+ * - Reset link is single-use and time-limited
255
+ * - Old password remains valid until reset is completed
256
+ *
257
+ * @example
258
+ * ```tsx
259
+ * const { data, error } = await authClient.forgetPassword({
260
+ * email: 'user@example.com'
261
+ * })
262
+ * // Email sent with reset link
263
+ * ```
264
+ */
265
+ // Note: forgetPassword() is the method for password reset (not forgotPassword)
266
+ // Example usage: await authClient.forgetPassword({ email })
267
+
268
+ /**
269
+ * Complete password reset.
270
+ *
271
+ * Finalizes the password reset process using the token sent to the user's email.
272
+ *
273
+ * @param resetData - Object containing newPassword and the reset token
274
+ * @returns Promise resolving when password is reset
275
+ *
276
+ * SECURITY:
277
+ * - Token is validated against server records
278
+ * - Token is single-use and expires automatically
279
+ * - Old password is immediately invalidated
280
+ * - New password must meet strength requirements
281
+ *
282
+ * @example
283
+ * ```tsx
284
+ * const { data, error } = await resetPassword({
285
+ * newPassword: 'newSecurePassword123',
286
+ * token: 'reset-token-from-email'
287
+ * })
288
+ * // User can now sign in with new password
289
+ * ```
290
+ */
291
+ // Note: resetPassword() completes the password reset flow
292
+ // Example usage: await authClient.resetPassword({ newPassword, token })
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Email sending module using Resend API.
3
+ *
4
+ * This module provides functions to send transactional emails for user authentication
5
+ * flows including email verification and password reset. It uses the Resend service
6
+ * for reliable email delivery with built-in bounce handling and analytics.
7
+ *
8
+ * Key patterns and conventions:
9
+ * - All email sending is async and non-blocking for the main application flow
10
+ * - Errors are logged but don't expose detailed error messages to users
11
+ * (to prevent information leakage about email infrastructure)
12
+ * - HTML emails use inline styles for maximum client compatibility
13
+ * - Responsive design with max-width 600px for optimal mobile rendering
14
+ * - Verification/reset links are included both as buttons and plain text URLs
15
+ * to handle cases where buttons are blocked by email clients
16
+ *
17
+ * Security considerations:
18
+ * - RESEND_API_KEY is loaded from environment and never exposed to client
19
+ * - Email content is HTML-escaped by template literals to prevent XSS
20
+ * - Verification and reset URLs should be generated server-side with
21
+ * short-lived tokens (24 hours for verification, 1 hour for password reset)
22
+ * - From address uses noreply@yourdomain.com to prevent confusion
23
+ * - No user data beyond what's necessary is included in emails
24
+ * - Rate limiting should be implemented at the route level to prevent email spam
25
+ *
26
+ * Configuration decisions:
27
+ * - Short token expiration times (24h for verify, 1h for reset) balance security
28
+ * with user experience. Password reset tokens expire faster because they grant
29
+ * access to accounts.
30
+ * - Inline CSS styles instead of external stylesheets because many email clients
31
+ * block external stylesheets and the <style> tag
32
+ * - System font stack (Arial, sans-serif) for best rendering across all clients
33
+ * without loading external fonts
34
+ * - 600px max-width is industry standard for email newsletters and ensures good
35
+ * readability on both desktop and mobile devices
36
+ * - Blue (#0070f3) is used for CTA buttons as it has high contrast and is
37
+ * commonly recognized as an interactive element color
38
+ *
39
+ * Performance implications:
40
+ * - Email sending is I/O-bound and can take 200-1000ms per email
41
+ * - These functions should be called after database operations complete,
42
+ * so email failures don't prevent user actions
43
+ * - Consider using a queue system for high-volume email sending to avoid blocking
44
+ * - Errors are caught and re-thrown with generic messages to prevent information
45
+ * leakage while still allowing upstream handlers to take action
46
+ *
47
+ * Error handling strategy:
48
+ * - Errors are logged with context for debugging
49
+ * - Generic error messages are thrown to prevent exposing email service details
50
+ * - This allows the calling code to show user-friendly messages while
51
+ * maintaining security
52
+ *
53
+ * @example
54
+ * // Send verification email after user registration
55
+ * await sendVerificationEmail('user@example.com', 'https://app.com/verify/abc123')
56
+ *
57
+ * @example
58
+ * // Send password reset email after request
59
+ * await sendPasswordResetEmail('user@example.com', 'https://app.com/reset/xyz789')
60
+ */
61
+
62
+ import { Resend } from 'resend'
63
+ import { env } from '@/lib/env'
64
+
65
+ /**
66
+ * Resend API client instance.
67
+ *
68
+ * Initialized with the RESEND_API_KEY from environment variables.
69
+ * This instance is reused across all email sending operations to maintain
70
+ * a single connection and avoid multiple initializations.
71
+ */
72
+ const resend = new Resend(env.RESEND_API_KEY)
73
+
74
+ /**
75
+ * Sends a verification email to a newly registered user.
76
+ *
77
+ * This function sends an HTML email with a verification link that the user must
78
+ * click to confirm their email address. The link should be generated by the
79
+ * backend with a short-lived token (typically 24 hours).
80
+ *
81
+ * Performance characteristics:
82
+ * - Network latency: 200-1000ms typically
83
+ * - Non-blocking: Should be called after database write completes
84
+ * - No retry logic: Resend handles retries internally
85
+ *
86
+ * Security implications:
87
+ * - Verification URL should contain a cryptographically signed token
88
+ * - Token should expire after 24 hours to limit window for exploitation
89
+ * - Email content contains no sensitive data beyond the verification URL
90
+ * - URL is truncated in display to prevent layout issues with long URLs
91
+ *
92
+ * @param email - The recipient's email address. Should be validated before calling.
93
+ * @param verificationUrl - The full URL the user must click to verify their email.
94
+ * Should include a token with expiration.
95
+ * Example: https://app.com/verify/abc123token
96
+ * @returns Promise that resolves when email is sent successfully.
97
+ * @throws {Error} If email sending fails (network error, API error, etc.).
98
+ * Error message is generic to prevent information leakage.
99
+ *
100
+ * @example
101
+ * try {
102
+ * await sendVerificationEmail('user@example.com', 'https://app.com/verify/token123')
103
+ * console.log('Verification email sent')
104
+ * } catch (error) {
105
+ * console.error('Failed to send verification email')
106
+ * // Show user-friendly message: "Email sent. Please check your inbox."
107
+ * }
108
+ */
109
+ export async function sendVerificationEmail(
110
+ email: string,
111
+ verificationUrl: string
112
+ ) {
113
+ try {
114
+ await resend.emails.send({
115
+ from: 'noreply@yourdomain.com',
116
+ to: email,
117
+ subject: 'Verify your email address',
118
+ html: `
119
+ <!DOCTYPE html>
120
+ <html>
121
+ <head>
122
+ <meta charset="utf-8">
123
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
124
+ <title>Verify Your Email</title>
125
+ </head>
126
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
127
+ <div style="background: #f4f4f4; padding: 30px; border-radius: 8px;">
128
+ <h1 style="color: #333; margin-bottom: 20px;">Welcome!</h1>
129
+ <p style="margin-bottom: 20px;">Thank you for signing up. Please click the button below to verify your email address:</p>
130
+ <div style="text-align: center; margin: 30px 0;">
131
+ <a href="${verificationUrl}" style="display: inline-block; padding: 12px 30px; background: #0070f3; color: #fff; text-decoration: none; border-radius: 5px; font-weight: bold;">Verify Email</a>
132
+ </div>
133
+ <p style="color: #666; font-size: 14px;">If the button doesn't work, you can copy and paste this link into your browser:</p>
134
+ <p style="color: #666; font-size: 12px; word-break: break-all;">${verificationUrl}</p>
135
+ <p style="margin-top: 30px; color: #999; font-size: 12px;">This link will expire in 24 hours.</p>
136
+ </div>
137
+ </body>
138
+ </html>
139
+ `,
140
+ })
141
+ } catch (error) {
142
+ console.error('Failed to send verification email:', error)
143
+ throw new Error('Failed to send verification email')
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Sends a password reset email to a user who requested a password reset.
149
+ *
150
+ * This function sends an HTML email with a password reset link. The link should be
151
+ * generated by the backend with a short-lived token (typically 1 hour) to limit
152
+ * the window for potential account takeover attempts.
153
+ *
154
+ * Performance characteristics:
155
+ * - Network latency: 200-1000ms typically
156
+ * - Non-blocking: Should be called after database write completes
157
+ * - No retry logic: Resend handles retries internally
158
+ *
159
+ * Security implications:
160
+ * - Reset URL should contain a cryptographically signed token
161
+ * - Token should expire after 1 hour to limit account takeover window
162
+ * - Email confirms that a reset was requested (prevents silent resets)
163
+ * - If the user didn't request a reset, this serves as an early warning
164
+ * of potential unauthorized access attempts
165
+ * - No account details are revealed to prevent email enumeration attacks
166
+ * (email is sent even if the account doesn't exist in production)
167
+ *
168
+ * @param email - The recipient's email address. May not exist in the system.
169
+ * In production, send even for non-existent accounts to prevent
170
+ * email enumeration attacks.
171
+ * @param resetUrl - The full URL the user must click to reset their password.
172
+ * Should include a token with short expiration (1 hour recommended).
173
+ * Example: https://app.com/reset/xyz789token
174
+ * @returns Promise that resolves when email is sent successfully.
175
+ * @throws {Error} If email sending fails (network error, API error, etc.).
176
+ * Error message is generic to prevent information leakage.
177
+ *
178
+ * @example
179
+ * try {
180
+ * await sendPasswordResetEmail('user@example.com', 'https://app.com/reset/token456')
181
+ * console.log('Password reset email sent')
182
+ * } catch (error) {
183
+ * console.error('Failed to send password reset email')
184
+ * // Show user-friendly message: "If an account exists with this email,
185
+ * // you will receive a password reset link."
186
+ * }
187
+ */
188
+ export async function sendPasswordResetEmail(email: string, resetUrl: string) {
189
+ try {
190
+ await resend.emails.send({
191
+ from: 'noreply@yourdomain.com',
192
+ to: email,
193
+ subject: 'Reset Your Password',
194
+ html: `
195
+ <!DOCTYPE html>
196
+ <html>
197
+ <head>
198
+ <meta charset="utf-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
200
+ <title>Reset Your Password</title>
201
+ </head>
202
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
203
+ <div style="background: #f4f4f4; padding: 30px; border-radius: 8px;">
204
+ <h1 style="color: #333; margin-bottom: 20px;">Reset Password</h1>
205
+ <p style="margin-bottom: 20px;">We received a request to reset your password. Click the button below to set a new password:</p>
206
+ <div style="text-align: center; margin: 30px 0;">
207
+ <a href="${resetUrl}" style="display: inline-block; padding: 12px 30px; background: #0070f3; color: #fff; text-decoration: none; border-radius: 5px; font-weight: bold;">Reset Password</a>
208
+ </div>
209
+ <p style="color: #666; font-size: 14px;">If the button doesn't work, you can copy and paste this link into your browser:</p>
210
+ <p style="color: #666; font-size: 12px; word-break: break-all;">${resetUrl}</p>
211
+ <p style="margin-top: 30px; color: #999; font-size: 12px;">This link will expire in 1 hour. If you didn't request a password reset, you can safely ignore this email.</p>
212
+ </div>
213
+ </body>
214
+ </html>
215
+ `,
216
+ })
217
+ } catch (error) {
218
+ console.error('Failed to send password reset email:', error)
219
+ throw new Error('Failed to send password reset email')
220
+ }
221
+ }