shipd 0.1.2 → 0.1.4
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.
- package/base-package/app/globals.css +126 -0
- package/base-package/app/layout.tsx +53 -0
- package/base-package/app/page.tsx +15 -0
- package/base-package/base.config.json +57 -0
- package/base-package/components/ui/avatar.tsx +53 -0
- package/base-package/components/ui/badge.tsx +46 -0
- package/base-package/components/ui/button.tsx +59 -0
- package/base-package/components/ui/card.tsx +92 -0
- package/base-package/components/ui/chart.tsx +353 -0
- package/base-package/components/ui/checkbox.tsx +32 -0
- package/base-package/components/ui/dialog.tsx +135 -0
- package/base-package/components/ui/dropdown-menu.tsx +257 -0
- package/base-package/components/ui/form.tsx +167 -0
- package/base-package/components/ui/input.tsx +21 -0
- package/base-package/components/ui/label.tsx +24 -0
- package/base-package/components/ui/progress.tsx +31 -0
- package/base-package/components/ui/resizable.tsx +56 -0
- package/base-package/components/ui/select.tsx +185 -0
- package/base-package/components/ui/separator.tsx +28 -0
- package/base-package/components/ui/sheet.tsx +139 -0
- package/base-package/components/ui/skeleton.tsx +13 -0
- package/base-package/components/ui/sonner.tsx +25 -0
- package/base-package/components/ui/switch.tsx +31 -0
- package/base-package/components/ui/tabs.tsx +66 -0
- package/base-package/components/ui/textarea.tsx +18 -0
- package/base-package/components/ui/toggle-group.tsx +73 -0
- package/base-package/components/ui/toggle.tsx +47 -0
- package/base-package/components/ui/tooltip.tsx +61 -0
- package/base-package/components.json +21 -0
- package/base-package/eslint.config.mjs +16 -0
- package/base-package/lib/utils.ts +6 -0
- package/base-package/middleware.ts +12 -0
- package/base-package/next.config.ts +27 -0
- package/base-package/package.json +49 -0
- package/base-package/postcss.config.mjs +5 -0
- package/base-package/public/favicon.svg +4 -0
- package/base-package/tailwind.config.ts +89 -0
- package/base-package/tsconfig.json +27 -0
- package/dist/index.js +1862 -948
- package/features/ai-chat/README.md +258 -0
- package/features/ai-chat/app/api/chat/route.ts +16 -0
- package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
- package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
- package/features/ai-chat/feature.config.json +22 -0
- package/features/analytics/README.md +308 -0
- package/features/analytics/feature.config.json +20 -0
- package/features/analytics/lib/posthog.ts +36 -0
- package/features/auth/README.md +336 -0
- package/features/auth/app/api/auth/[...all]/route.ts +4 -0
- package/features/auth/app/dashboard/layout.tsx +15 -0
- package/features/auth/app/dashboard/page.tsx +140 -0
- package/features/auth/app/sign-in/page.tsx +228 -0
- package/features/auth/app/sign-up/page.tsx +243 -0
- package/features/auth/auth-schema.ts +47 -0
- package/features/auth/components/auth/setup-instructions.tsx +123 -0
- package/features/auth/feature.config.json +33 -0
- package/features/auth/lib/auth-client.ts +8 -0
- package/features/auth/lib/auth.ts +295 -0
- package/features/auth/lib/email-stub.ts +55 -0
- package/features/auth/lib/email.ts +47 -0
- package/features/auth/middleware.patch.ts +43 -0
- package/features/database/README.md +256 -0
- package/features/database/db/drizzle.ts +48 -0
- package/features/database/db/schema.ts +21 -0
- package/features/database/drizzle.config.ts +13 -0
- package/features/database/feature.config.json +30 -0
- package/features/email/README.md +282 -0
- package/features/email/emails/components/layout.tsx +181 -0
- package/features/email/emails/password-reset.tsx +67 -0
- package/features/email/emails/payment-failed.tsx +167 -0
- package/features/email/emails/subscription-confirmation.tsx +129 -0
- package/features/email/emails/welcome.tsx +100 -0
- package/features/email/feature.config.json +22 -0
- package/features/email/lib/email.ts +118 -0
- package/features/file-upload/README.md +271 -0
- package/features/file-upload/app/api/upload-image/route.ts +64 -0
- package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
- package/features/file-upload/feature.config.json +23 -0
- package/features/file-upload/lib/upload-image.ts +28 -0
- package/features/marketing-landing/README.md +266 -0
- package/features/marketing-landing/app/page.tsx +25 -0
- package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
- package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
- package/features/marketing-landing/components/homepage/footer.tsx +53 -0
- package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
- package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
- package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
- package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
- package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
- package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
- package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
- package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
- package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
- package/features/marketing-landing/components/logos/Polar.tsx +7 -0
- package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
- package/features/marketing-landing/components/logos/index.ts +6 -0
- package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
- package/features/marketing-landing/feature.config.json +23 -0
- package/features/payments/README.md +306 -0
- package/features/payments/app/api/subscription/route.ts +25 -0
- package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
- package/features/payments/app/dashboard/payment/page.tsx +126 -0
- package/features/payments/app/success/page.tsx +123 -0
- package/features/payments/feature.config.json +31 -0
- package/features/payments/lib/polar-products.ts +49 -0
- package/features/payments/lib/subscription.ts +148 -0
- package/features/payments/payments-schema.ts +30 -0
- package/features/seo/README.md +244 -0
- package/features/seo/app/blog/[slug]/page.tsx +314 -0
- package/features/seo/app/blog/page.tsx +107 -0
- package/features/seo/app/robots.txt +13 -0
- package/features/seo/app/sitemap.ts +70 -0
- package/features/seo/feature.config.json +19 -0
- package/features/seo/lib/seo-utils.ts +163 -0
- package/package.json +3 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { db } from "@/db/drizzle";
|
|
2
|
+
import { account, session, user, verification } from "@/db/schema";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
// Subscription is optional (only if payments module is installed)
|
|
5
|
+
// We'll check for it at runtime in the webhook handler
|
|
6
|
+
import {
|
|
7
|
+
checkout,
|
|
8
|
+
polar,
|
|
9
|
+
portal,
|
|
10
|
+
usage,
|
|
11
|
+
webhooks,
|
|
12
|
+
} from "@polar-sh/better-auth";
|
|
13
|
+
import { Polar } from "@polar-sh/sdk";
|
|
14
|
+
import { betterAuth } from "better-auth";
|
|
15
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
16
|
+
import { nextCookies } from "better-auth/next-js";
|
|
17
|
+
import {
|
|
18
|
+
sendPasswordResetEmail,
|
|
19
|
+
sendSubscriptionConfirmationEmail,
|
|
20
|
+
sendPaymentFailedEmail,
|
|
21
|
+
} from "@/lib/email";
|
|
22
|
+
|
|
23
|
+
// Utility function to safely parse dates
|
|
24
|
+
function safeParseDate(value: string | Date | null | undefined): Date | null {
|
|
25
|
+
if (!value) return null;
|
|
26
|
+
if (value instanceof Date) return value;
|
|
27
|
+
return new Date(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check if Polar is configured
|
|
31
|
+
const isPolarConfigured = !!(
|
|
32
|
+
process.env.POLAR_ACCESS_TOKEN &&
|
|
33
|
+
process.env.POLAR_WEBHOOK_SECRET &&
|
|
34
|
+
process.env.NEXT_PUBLIC_STARTER_TIER &&
|
|
35
|
+
process.env.NEXT_PUBLIC_STARTER_SLUG
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!isPolarConfigured && process.env.NODE_ENV === "development") {
|
|
39
|
+
console.warn(
|
|
40
|
+
"⚠️ Polar.sh not configured - subscription features disabled. Configure POLAR_* env vars to enable payments.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const polarClient = isPolarConfigured
|
|
45
|
+
? new Polar({
|
|
46
|
+
accessToken: process.env.POLAR_ACCESS_TOKEN,
|
|
47
|
+
})
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
export const auth = betterAuth({
|
|
51
|
+
trustedOrigins: [`${process.env.NEXT_PUBLIC_APP_URL}`],
|
|
52
|
+
allowedDevOrigins: [`${process.env.NEXT_PUBLIC_APP_URL}`],
|
|
53
|
+
cookieCache: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
maxAge: 5 * 60, // Cache duration in seconds
|
|
56
|
+
},
|
|
57
|
+
database: drizzleAdapter(db, {
|
|
58
|
+
provider: "pg",
|
|
59
|
+
schema: {
|
|
60
|
+
user,
|
|
61
|
+
session,
|
|
62
|
+
account,
|
|
63
|
+
verification,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
emailAndPassword: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
sendResetPassword: async ({ user, url }) => {
|
|
69
|
+
// Send password reset email
|
|
70
|
+
await sendPasswordResetEmail(user.email, url);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
user: {
|
|
74
|
+
additionalFields: {
|
|
75
|
+
name: {
|
|
76
|
+
type: "string",
|
|
77
|
+
required: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
socialProviders: {
|
|
82
|
+
google: {
|
|
83
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
84
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
// TODO: Implement welcome emails via a custom sign-up API route
|
|
88
|
+
// Better Auth hooks API may have changed - using webhook approach instead
|
|
89
|
+
plugins: [
|
|
90
|
+
...(isPolarConfigured && polarClient
|
|
91
|
+
? [
|
|
92
|
+
polar({
|
|
93
|
+
client: polarClient,
|
|
94
|
+
createCustomerOnSignUp: true,
|
|
95
|
+
use: [
|
|
96
|
+
checkout({
|
|
97
|
+
products: [
|
|
98
|
+
{
|
|
99
|
+
productId: process.env.NEXT_PUBLIC_STARTER_TIER!,
|
|
100
|
+
slug: process.env.NEXT_PUBLIC_STARTER_SLUG!,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/${process.env.POLAR_SUCCESS_URL}`,
|
|
104
|
+
authenticatedUsersOnly: true,
|
|
105
|
+
}),
|
|
106
|
+
portal(),
|
|
107
|
+
usage(),
|
|
108
|
+
webhooks({
|
|
109
|
+
secret: process.env.POLAR_WEBHOOK_SECRET!,
|
|
110
|
+
onPayload: async ({ data, type }) => {
|
|
111
|
+
if (
|
|
112
|
+
type === "subscription.created" ||
|
|
113
|
+
type === "subscription.active" ||
|
|
114
|
+
type === "subscription.canceled" ||
|
|
115
|
+
type === "subscription.revoked" ||
|
|
116
|
+
type === "subscription.uncanceled" ||
|
|
117
|
+
type === "subscription.updated"
|
|
118
|
+
) {
|
|
119
|
+
console.log("🎯 Processing subscription webhook:", type);
|
|
120
|
+
console.log("📦 Payload data:", JSON.stringify(data, null, 2));
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// STEP 1: Extract user ID from customer data
|
|
124
|
+
const userId = data.customer?.externalId;
|
|
125
|
+
// STEP 2: Build subscription data
|
|
126
|
+
const subscriptionData = {
|
|
127
|
+
id: data.id,
|
|
128
|
+
createdAt: new Date(data.createdAt),
|
|
129
|
+
modifiedAt: safeParseDate(data.modifiedAt),
|
|
130
|
+
amount: data.amount,
|
|
131
|
+
currency: data.currency,
|
|
132
|
+
recurringInterval: data.recurringInterval,
|
|
133
|
+
status: data.status,
|
|
134
|
+
currentPeriodStart:
|
|
135
|
+
safeParseDate(data.currentPeriodStart) || new Date(),
|
|
136
|
+
currentPeriodEnd:
|
|
137
|
+
safeParseDate(data.currentPeriodEnd) || new Date(),
|
|
138
|
+
cancelAtPeriodEnd: data.cancelAtPeriodEnd || false,
|
|
139
|
+
canceledAt: safeParseDate(data.canceledAt),
|
|
140
|
+
startedAt: safeParseDate(data.startedAt) || new Date(),
|
|
141
|
+
endsAt: safeParseDate(data.endsAt),
|
|
142
|
+
endedAt: safeParseDate(data.endedAt),
|
|
143
|
+
customerId: data.customerId,
|
|
144
|
+
productId: data.productId,
|
|
145
|
+
discountId: data.discountId || null,
|
|
146
|
+
checkoutId: data.checkoutId || "",
|
|
147
|
+
customerCancellationReason:
|
|
148
|
+
data.customerCancellationReason || null,
|
|
149
|
+
customerCancellationComment:
|
|
150
|
+
data.customerCancellationComment || null,
|
|
151
|
+
metadata: data.metadata
|
|
152
|
+
? JSON.stringify(data.metadata)
|
|
153
|
+
: null,
|
|
154
|
+
customFieldData: data.customFieldData
|
|
155
|
+
? JSON.stringify(data.customFieldData)
|
|
156
|
+
: null,
|
|
157
|
+
userId: userId as string | null,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
console.log("💾 Final subscription data:", {
|
|
161
|
+
id: subscriptionData.id,
|
|
162
|
+
status: subscriptionData.status,
|
|
163
|
+
userId: subscriptionData.userId,
|
|
164
|
+
amount: subscriptionData.amount,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// STEP 3: Use Drizzle's onConflictDoUpdate for proper upsert
|
|
168
|
+
// Only if subscription table exists (payments module installed)
|
|
169
|
+
// Dynamically import subscription table
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
let subscriptionTable: any;
|
|
172
|
+
try {
|
|
173
|
+
const schemaModule = await import("@/db/schema");
|
|
174
|
+
if ('subscription' in schemaModule && schemaModule.subscription) {
|
|
175
|
+
subscriptionTable = schemaModule.subscription;
|
|
176
|
+
} else {
|
|
177
|
+
console.warn("⚠️ Subscription table not found. Install payments module to enable subscription webhooks.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
console.warn("⚠️ Subscription table not found. Install payments module to enable subscription webhooks.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await db
|
|
186
|
+
.insert(subscriptionTable)
|
|
187
|
+
.values(subscriptionData)
|
|
188
|
+
.onConflictDoUpdate({
|
|
189
|
+
target: subscriptionTable.id,
|
|
190
|
+
set: {
|
|
191
|
+
modifiedAt: subscriptionData.modifiedAt || new Date(),
|
|
192
|
+
amount: subscriptionData.amount,
|
|
193
|
+
currency: subscriptionData.currency,
|
|
194
|
+
recurringInterval: subscriptionData.recurringInterval,
|
|
195
|
+
status: subscriptionData.status,
|
|
196
|
+
currentPeriodStart: subscriptionData.currentPeriodStart,
|
|
197
|
+
currentPeriodEnd: subscriptionData.currentPeriodEnd,
|
|
198
|
+
cancelAtPeriodEnd: subscriptionData.cancelAtPeriodEnd,
|
|
199
|
+
canceledAt: subscriptionData.canceledAt,
|
|
200
|
+
startedAt: subscriptionData.startedAt,
|
|
201
|
+
endsAt: subscriptionData.endsAt,
|
|
202
|
+
endedAt: subscriptionData.endedAt,
|
|
203
|
+
customerId: subscriptionData.customerId,
|
|
204
|
+
productId: subscriptionData.productId,
|
|
205
|
+
discountId: subscriptionData.discountId,
|
|
206
|
+
checkoutId: subscriptionData.checkoutId,
|
|
207
|
+
customerCancellationReason:
|
|
208
|
+
subscriptionData.customerCancellationReason,
|
|
209
|
+
customerCancellationComment:
|
|
210
|
+
subscriptionData.customerCancellationComment,
|
|
211
|
+
metadata: subscriptionData.metadata,
|
|
212
|
+
customFieldData: subscriptionData.customFieldData,
|
|
213
|
+
userId: subscriptionData.userId,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.log("✅ Upserted subscription:", data.id);
|
|
218
|
+
|
|
219
|
+
// Send subscription emails based on event type
|
|
220
|
+
try {
|
|
221
|
+
// Get user email from database
|
|
222
|
+
if (userId) {
|
|
223
|
+
const userRecord = await db
|
|
224
|
+
.select()
|
|
225
|
+
.from(user)
|
|
226
|
+
.where(eq(user.id, userId))
|
|
227
|
+
.limit(1);
|
|
228
|
+
|
|
229
|
+
if (userRecord[0]?.email) {
|
|
230
|
+
const userEmail = userRecord[0].email;
|
|
231
|
+
|
|
232
|
+
// Send confirmation email for new or reactivated subscriptions
|
|
233
|
+
if (
|
|
234
|
+
type === "subscription.created" ||
|
|
235
|
+
type === "subscription.active"
|
|
236
|
+
) {
|
|
237
|
+
const amount = `$${(data.amount / 100).toFixed(2)}/${data.recurringInterval}`;
|
|
238
|
+
await sendSubscriptionConfirmationEmail(
|
|
239
|
+
userEmail,
|
|
240
|
+
"Premium", // TODO: Get actual plan name from product
|
|
241
|
+
amount
|
|
242
|
+
);
|
|
243
|
+
console.log("📧 Sent subscription confirmation email to:", userEmail);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (emailError) {
|
|
248
|
+
console.error("📧 Failed to send subscription email:", emailError);
|
|
249
|
+
// Don't throw - email failures shouldn't block webhook processing
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(
|
|
253
|
+
"💥 Error processing subscription webhook:",
|
|
254
|
+
error,
|
|
255
|
+
);
|
|
256
|
+
// Don't throw - let webhook succeed to avoid retries
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle payment failed events
|
|
261
|
+
if (type === "subscription.payment_failed") {
|
|
262
|
+
try {
|
|
263
|
+
const userId = data.customer?.externalId;
|
|
264
|
+
if (userId) {
|
|
265
|
+
const userRecord = await db
|
|
266
|
+
.select()
|
|
267
|
+
.from(user)
|
|
268
|
+
.where(eq(user.id, userId))
|
|
269
|
+
.limit(1);
|
|
270
|
+
|
|
271
|
+
if (userRecord[0]?.email) {
|
|
272
|
+
const retryDate = new Date();
|
|
273
|
+
retryDate.setDate(retryDate.getDate() + 3); // 3 days from now
|
|
274
|
+
|
|
275
|
+
await sendPaymentFailedEmail(
|
|
276
|
+
userRecord[0].email,
|
|
277
|
+
"Premium", // TODO: Get actual plan name
|
|
278
|
+
retryDate.toLocaleDateString()
|
|
279
|
+
);
|
|
280
|
+
console.log("📧 Sent payment failed email to:", userRecord[0].email);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("📧 Failed to send payment failed email:", error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
],
|
|
290
|
+
}),
|
|
291
|
+
]
|
|
292
|
+
: []),
|
|
293
|
+
nextCookies(),
|
|
294
|
+
],
|
|
295
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email stub for auth module
|
|
3
|
+
*
|
|
4
|
+
* This provides minimal email functionality when the full email module is not installed.
|
|
5
|
+
* Email sending will be logged to console in development.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface EmailOptions {
|
|
9
|
+
to: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
user?: { email: string; name?: string };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send password reset email (stub)
|
|
17
|
+
*/
|
|
18
|
+
export async function sendPasswordResetEmail(
|
|
19
|
+
email: string,
|
|
20
|
+
url: string
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (process.env.NODE_ENV === 'development') {
|
|
23
|
+
console.log(`📧 [Email Stub] Password reset email would be sent to ${email}`);
|
|
24
|
+
console.log(` Reset URL: ${url}`);
|
|
25
|
+
console.log(` Install the email module for actual email sending.`);
|
|
26
|
+
}
|
|
27
|
+
// In production, this would fail silently or you should install the email module
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Send subscription confirmation email (stub)
|
|
32
|
+
*/
|
|
33
|
+
export async function sendSubscriptionConfirmationEmail(
|
|
34
|
+
email: string,
|
|
35
|
+
subscriptionDetails: any
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
if (process.env.NODE_ENV === 'development') {
|
|
38
|
+
console.log(`📧 [Email Stub] Subscription confirmation email would be sent to ${email}`);
|
|
39
|
+
console.log(` Install the email module for actual email sending.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send payment failed email (stub)
|
|
45
|
+
*/
|
|
46
|
+
export async function sendPaymentFailedEmail(
|
|
47
|
+
email: string,
|
|
48
|
+
errorDetails: any
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
if (process.env.NODE_ENV === 'development') {
|
|
51
|
+
console.log(`📧 [Email Stub] Payment failed email would be sent to ${email}`);
|
|
52
|
+
console.log(` Install the email module for actual email sending.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email functions for auth module
|
|
3
|
+
*
|
|
4
|
+
* This is a minimal email implementation that logs to console.
|
|
5
|
+
* Install the full email module (Resend) for production email sending.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send password reset email
|
|
10
|
+
*/
|
|
11
|
+
export async function sendPasswordResetEmail(
|
|
12
|
+
email: string,
|
|
13
|
+
url: string
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
if (process.env.NODE_ENV === 'development') {
|
|
16
|
+
console.log(`📧 [Email] Password reset email would be sent to ${email}`);
|
|
17
|
+
console.log(` Reset URL: ${url}`);
|
|
18
|
+
console.log(` Install the email module (Resend) for actual email sending.`);
|
|
19
|
+
}
|
|
20
|
+
// In production without email module, this fails silently
|
|
21
|
+
// Install the email module for production use
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send subscription confirmation email
|
|
26
|
+
*/
|
|
27
|
+
export async function sendSubscriptionConfirmationEmail(
|
|
28
|
+
email: string
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (process.env.NODE_ENV === 'development') {
|
|
31
|
+
console.log(`📧 [Email] Subscription confirmation email would be sent to ${email}`);
|
|
32
|
+
console.log(` Install the email module (Resend) for actual email sending.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send payment failed email
|
|
38
|
+
*/
|
|
39
|
+
export async function sendPaymentFailedEmail(
|
|
40
|
+
email: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if (process.env.NODE_ENV === 'development') {
|
|
43
|
+
console.log(`📧 [Email] Payment failed email would be sent to ${email}`);
|
|
44
|
+
console.log(` Install the email module (Resend) for actual email sending.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Middleware Patch
|
|
3
|
+
*
|
|
4
|
+
* This file contains the middleware logic for authentication.
|
|
5
|
+
* When the auth module is appended, this will be merged into the base middleware.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
9
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Apply auth middleware logic
|
|
13
|
+
* This function should be called from the base middleware
|
|
14
|
+
*/
|
|
15
|
+
export function applyAuthMiddleware(
|
|
16
|
+
request: NextRequest,
|
|
17
|
+
baseResponse: NextResponse
|
|
18
|
+
): NextResponse | null {
|
|
19
|
+
const sessionCookie = getSessionCookie(request);
|
|
20
|
+
const { pathname } = request.nextUrl;
|
|
21
|
+
|
|
22
|
+
// Don't redirect from sign-in/sign-up pages to avoid redirect loops
|
|
23
|
+
if (["/sign-in", "/sign-up"].includes(pathname)) {
|
|
24
|
+
return null; // Let base middleware handle it
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Redirect unauthenticated users from protected routes
|
|
28
|
+
const protectedRoutes = ["/dashboard"]; // Can be customized
|
|
29
|
+
const isProtectedRoute = protectedRoutes.some(route => pathname.startsWith(route));
|
|
30
|
+
|
|
31
|
+
if (isProtectedRoute && !sessionCookie) {
|
|
32
|
+
const returnTo = encodeURIComponent(pathname);
|
|
33
|
+
return NextResponse.redirect(new URL(`/sign-in?returnTo=${returnTo}`, request.url));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null; // Continue with base middleware
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Middleware matcher configuration for auth
|
|
41
|
+
*/
|
|
42
|
+
export const matcher = ["/dashboard/:path*", "/sign-in", "/sign-up"];
|
|
43
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Database Module - Integration Guide
|
|
2
|
+
|
|
3
|
+
**Module Version:** 1.0.0
|
|
4
|
+
**Last Updated:** 2025-12-22
|
|
5
|
+
**Standalone:** ✅ Yes (base database setup)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This module sets up PostgreSQL database connectivity using Drizzle ORM. It provides the foundation for database operations and can be extended by other modules (auth, payments) that add their own tables.
|
|
12
|
+
|
|
13
|
+
**Key Features:**
|
|
14
|
+
- PostgreSQL connection with Drizzle ORM
|
|
15
|
+
- Drizzle Kit configuration for migrations
|
|
16
|
+
- Base schema structure (extended by other modules)
|
|
17
|
+
- Database scripts (push, generate, migrate, studio)
|
|
18
|
+
- Support for Neon, Supabase, or any Postgres database
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Files Added
|
|
23
|
+
|
|
24
|
+
### Configuration
|
|
25
|
+
- `drizzle.config.ts` - Drizzle Kit configuration
|
|
26
|
+
- `db/drizzle.ts` - Database connection setup
|
|
27
|
+
- `db/schema.ts` - Base schema file (modules will add tables here)
|
|
28
|
+
|
|
29
|
+
### Migrations
|
|
30
|
+
- `db/migrations/` - Directory for database migrations
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Dependencies
|
|
35
|
+
|
|
36
|
+
### Package Dependencies
|
|
37
|
+
The following packages will be added to `package.json`:
|
|
38
|
+
- `postgres@^3.4.4` - PostgreSQL client
|
|
39
|
+
- `drizzle-orm@^0.41.0` - Drizzle ORM
|
|
40
|
+
|
|
41
|
+
### Dev Dependencies
|
|
42
|
+
- `drizzle-kit@^0.31.0` - Drizzle migration and studio tools
|
|
43
|
+
|
|
44
|
+
### Package Scripts
|
|
45
|
+
The following scripts will be added to `package.json`:
|
|
46
|
+
- `db:push` - Push schema changes to database (development)
|
|
47
|
+
- `db:generate` - Generate migration files
|
|
48
|
+
- `db:migrate` - Run migrations
|
|
49
|
+
- `db:studio` - Open Drizzle Studio (database GUI)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Environment Variables
|
|
54
|
+
|
|
55
|
+
Add this to your `.env.local`:
|
|
56
|
+
|
|
57
|
+
```env
|
|
58
|
+
# Database
|
|
59
|
+
DATABASE_URL="postgresql://user:password@host:port/database"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Required Variables:**
|
|
63
|
+
- `DATABASE_URL` - PostgreSQL connection string
|
|
64
|
+
|
|
65
|
+
**Where to get it:**
|
|
66
|
+
- **Neon**: [neon.tech](https://neon.tech) - Free tier available
|
|
67
|
+
- **Supabase**: [supabase.com](https://supabase.com) - Free tier available
|
|
68
|
+
- **Local Postgres**: `postgresql://postgres:password@localhost:5432/mydb`
|
|
69
|
+
- **Any Postgres**: Standard PostgreSQL connection string format
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Manual Integration Steps
|
|
74
|
+
|
|
75
|
+
If you're appending this module to an existing project, follow these steps:
|
|
76
|
+
|
|
77
|
+
### Step 1: Install Dependencies
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install
|
|
81
|
+
# or
|
|
82
|
+
pnpm install
|
|
83
|
+
# or
|
|
84
|
+
yarn install
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The append command automatically adds missing dependencies to `package.json`, but you need to install them.
|
|
88
|
+
|
|
89
|
+
### Step 2: Add Environment Variable
|
|
90
|
+
|
|
91
|
+
1. Copy `.env.example` to `.env.local` (if not exists)
|
|
92
|
+
2. Add `DATABASE_URL` with your PostgreSQL connection string
|
|
93
|
+
3. Get a database URL from:
|
|
94
|
+
- [Neon](https://neon.tech) (recommended for development)
|
|
95
|
+
- [Supabase](https://supabase.com)
|
|
96
|
+
- Your own PostgreSQL server
|
|
97
|
+
|
|
98
|
+
### Step 3: Verify Installation
|
|
99
|
+
|
|
100
|
+
1. Check that `drizzle.config.ts` exists in project root
|
|
101
|
+
2. Check that `db/drizzle.ts` and `db/schema.ts` exist
|
|
102
|
+
3. Verify package.json has database scripts
|
|
103
|
+
|
|
104
|
+
### Step 4: Initialize Database (Optional)
|
|
105
|
+
|
|
106
|
+
If you want to create an initial migration:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm run db:generate
|
|
110
|
+
npm run db:push
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Note:** The base schema is empty. Tables will be added when you install other modules (auth, payments, etc.).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Usage Examples
|
|
118
|
+
|
|
119
|
+
### Using the Database Connection
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { db } from "@/db/drizzle";
|
|
123
|
+
|
|
124
|
+
// Example query (after auth module adds user table)
|
|
125
|
+
const users = await db.select().from(user);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Generating Migrations
|
|
129
|
+
|
|
130
|
+
After adding tables (via auth or payments modules):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Generate migration files
|
|
134
|
+
npm run db:generate
|
|
135
|
+
|
|
136
|
+
# Review the generated migration in db/migrations/
|
|
137
|
+
|
|
138
|
+
# Push to database (development)
|
|
139
|
+
npm run db:push
|
|
140
|
+
|
|
141
|
+
# Or run migrations (production)
|
|
142
|
+
npm run db:migrate
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Opening Drizzle Studio
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run db:studio
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This opens a web-based database GUI at `http://localhost:4983`
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Customization
|
|
156
|
+
|
|
157
|
+
### Database Connection
|
|
158
|
+
|
|
159
|
+
Edit `db/drizzle.ts` to customize the connection:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// For Supabase compatibility (prepared statements disabled)
|
|
163
|
+
const client = postgres(connectionString, { prepare: false });
|
|
164
|
+
|
|
165
|
+
// For other Postgres providers
|
|
166
|
+
const client = postgres(connectionString);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Schema Location
|
|
170
|
+
|
|
171
|
+
The schema file is at `db/schema.ts`. Other modules will add their tables here. You can also add custom tables:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// db/schema.ts
|
|
175
|
+
export const myCustomTable = pgTable("my_custom_table", {
|
|
176
|
+
id: text("id").primaryKey(),
|
|
177
|
+
name: text("name").notNull(),
|
|
178
|
+
// ...
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Integration Points
|
|
185
|
+
|
|
186
|
+
### Files That May Need Manual Updates
|
|
187
|
+
|
|
188
|
+
**db/schema.ts**:
|
|
189
|
+
- Other modules (auth, payments) will add their tables to this file
|
|
190
|
+
- You can add custom tables here as well
|
|
191
|
+
|
|
192
|
+
**drizzle.config.ts**:
|
|
193
|
+
- Usually doesn't need changes
|
|
194
|
+
- Modify if you need custom migration settings
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Troubleshooting
|
|
199
|
+
|
|
200
|
+
### Common Issues
|
|
201
|
+
|
|
202
|
+
**Issue:** "Missing DATABASE_URL" error
|
|
203
|
+
**Solution:**
|
|
204
|
+
- Ensure `DATABASE_URL` is set in `.env.local`
|
|
205
|
+
- Restart your dev server after adding env vars
|
|
206
|
+
- Check the connection string format
|
|
207
|
+
|
|
208
|
+
**Issue:** "Connection refused" or "ECONNREFUSED"
|
|
209
|
+
**Solution:**
|
|
210
|
+
- Verify your database is running
|
|
211
|
+
- Check the connection string is correct
|
|
212
|
+
- For cloud databases, ensure your IP is whitelisted (if required)
|
|
213
|
+
|
|
214
|
+
**Issue:** "relation does not exist" error
|
|
215
|
+
**Solution:**
|
|
216
|
+
- Run migrations: `npm run db:push` or `npm run db:migrate`
|
|
217
|
+
- Ensure other modules (auth, payments) have been installed and their tables added
|
|
218
|
+
|
|
219
|
+
**Issue:** Supabase connection issues
|
|
220
|
+
**Solution:**
|
|
221
|
+
- The connection is configured with `prepare: false` for Supabase compatibility
|
|
222
|
+
- If issues persist, check Supabase connection pooling settings
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Next Steps
|
|
227
|
+
|
|
228
|
+
After installing this module:
|
|
229
|
+
|
|
230
|
+
1. Set up your database (Neon, Supabase, or local)
|
|
231
|
+
2. Add `DATABASE_URL` to `.env.local`
|
|
232
|
+
3. Install Auth module (will add user/session tables)
|
|
233
|
+
4. Install Payments module (will add subscription table)
|
|
234
|
+
5. Run `npm run db:push` to create tables
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Related Modules
|
|
239
|
+
|
|
240
|
+
This module is required by:
|
|
241
|
+
- **Auth Module** - Adds user, session, account, verification tables
|
|
242
|
+
- **Payments Module** - Adds subscription table
|
|
243
|
+
|
|
244
|
+
This module works well with:
|
|
245
|
+
- **Auth Module** - For user authentication
|
|
246
|
+
- **Payments Module** - For subscription management
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Module Status
|
|
251
|
+
|
|
252
|
+
✅ **Standalone Package** - Can be installed independently
|
|
253
|
+
✅ **Base Setup** - Provides foundation for other modules
|
|
254
|
+
✅ **Ready to Use** - Works immediately after append
|
|
255
|
+
✅ **Well Documented** - Comprehensive integration guide
|
|
256
|
+
|