create-solostack 1.2.2 → 1.3.0
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/package.json +1 -1
- package/src/generators/base.js +6 -5
- package/src/generators/pro/admin.js +344 -0
- package/src/generators/pro/api-keys.js +464 -0
- package/src/generators/pro/database-full.js +233 -0
- package/src/generators/pro/emails.js +248 -0
- package/src/generators/pro/oauth.js +217 -0
- package/src/generators/pro/stripe-advanced.js +521 -0
- package/src/generators/setup.js +38 -21
- package/src/index.js +112 -4
- package/src/utils/license.js +83 -0
- package/src/utils/logger.js +33 -0
- package/src/utils/packages.js +14 -0
package/package.json
CHANGED
package/src/generators/base.js
CHANGED
|
@@ -398,20 +398,21 @@ export default function Home() {
|
|
|
398
398
|
icon={<Database className="w-5 h-5 text-emerald-400" />}
|
|
399
399
|
status={status?.database?.connected}
|
|
400
400
|
loading={loading}
|
|
401
|
-
description="PostgreSQL + Prisma"
|
|
401
|
+
description="${config?.database === 'Supabase' ? 'Supabase' : 'PostgreSQL + Prisma'}"
|
|
402
402
|
details={status?.database?.connected ? \`User count: \${status.database.userCount}\` : status?.database?.error || "Not connected"}
|
|
403
|
-
actionLabel="Open Studio"
|
|
404
|
-
actionCommand="npm run db:studio"
|
|
403
|
+
actionLabel="${config?.database === 'Supabase' ? 'Dashboard' : 'Open Studio'}"
|
|
404
|
+
actionCommand="${config?.database === 'Supabase' ? '' : 'npm run db:studio'}"
|
|
405
|
+
actionUrl="${config?.database === 'Supabase' ? 'https://supabase.com/dashboard' : ''}"
|
|
405
406
|
/>
|
|
406
407
|
<StatusCard
|
|
407
408
|
title="Authentication"
|
|
408
409
|
icon={<Shield className="w-5 h-5 text-blue-400" />}
|
|
409
410
|
status={status?.auth?.configured}
|
|
410
411
|
loading={loading}
|
|
411
|
-
description={status?.auth?.providers?.map((p: any) => p.name).join(', ') || "
|
|
412
|
+
description={status?.auth?.type || status?.auth?.providers?.map((p: any) => p.name).join(', ') || "Not configured"}
|
|
412
413
|
details={status?.auth?.configured ? "Ready to authenticate users" : "Missing secrets"}
|
|
413
414
|
actionLabel="Read Docs"
|
|
414
|
-
actionUrl="https://next-auth.js.org"
|
|
415
|
+
actionUrl="${config?.auth === 'Supabase Auth' ? 'https://supabase.com/docs/guides/auth' : 'https://next-auth.js.org'}"
|
|
415
416
|
/>
|
|
416
417
|
<StatusCard
|
|
417
418
|
title="Payments"
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Panel Generator - Pro Feature
|
|
3
|
+
* Generates admin dashboard with user management and analytics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, ensureDir } from '../../utils/files.js';
|
|
8
|
+
|
|
9
|
+
export async function generateAdmin(projectPath) {
|
|
10
|
+
await ensureDir(path.join(projectPath, 'src/app/admin'));
|
|
11
|
+
await ensureDir(path.join(projectPath, 'src/app/admin/users'));
|
|
12
|
+
await ensureDir(path.join(projectPath, 'src/app/admin/subscriptions'));
|
|
13
|
+
await ensureDir(path.join(projectPath, 'src/components/admin'));
|
|
14
|
+
|
|
15
|
+
// Generate admin layout
|
|
16
|
+
const adminLayout = `import { redirect } from 'next/navigation';
|
|
17
|
+
import { auth } from '@/lib/auth';
|
|
18
|
+
import Link from 'next/link';
|
|
19
|
+
import { LayoutDashboard, Users, CreditCard, BarChart3, ArrowLeft } from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
export default async function AdminLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const session = await auth();
|
|
27
|
+
|
|
28
|
+
if (!session || session.user.role !== 'ADMIN') {
|
|
29
|
+
redirect('/login');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const navItems = [
|
|
33
|
+
{ href: '/admin', label: 'Dashboard', icon: LayoutDashboard },
|
|
34
|
+
{ href: '/admin/users', label: 'Users', icon: Users },
|
|
35
|
+
{ href: '/admin/subscriptions', label: 'Subscriptions', icon: CreditCard },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="min-h-screen bg-gray-50">
|
|
40
|
+
{/* Sidebar */}
|
|
41
|
+
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r bg-white">
|
|
42
|
+
<div className="flex h-full flex-col">
|
|
43
|
+
<div className="flex h-16 items-center border-b px-6">
|
|
44
|
+
<span className="text-lg font-bold text-gray-900">Admin Panel</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<nav className="flex-1 space-y-1 px-3 py-4">
|
|
48
|
+
{navItems.map((item) => (
|
|
49
|
+
<Link
|
|
50
|
+
key={item.href}
|
|
51
|
+
href={item.href}
|
|
52
|
+
className="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
53
|
+
>
|
|
54
|
+
<item.icon className="h-5 w-5" />
|
|
55
|
+
{item.label}
|
|
56
|
+
</Link>
|
|
57
|
+
))}
|
|
58
|
+
</nav>
|
|
59
|
+
|
|
60
|
+
<div className="border-t p-4">
|
|
61
|
+
<Link
|
|
62
|
+
href="/dashboard"
|
|
63
|
+
className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-900"
|
|
64
|
+
>
|
|
65
|
+
<ArrowLeft className="h-4 w-4" />
|
|
66
|
+
Back to App
|
|
67
|
+
</Link>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</aside>
|
|
71
|
+
|
|
72
|
+
{/* Main content */}
|
|
73
|
+
<main className="ml-64 min-h-screen p-8">
|
|
74
|
+
{children}
|
|
75
|
+
</main>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
await writeFile(
|
|
82
|
+
path.join(projectPath, 'src/app/admin/layout.tsx'),
|
|
83
|
+
adminLayout
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Generate admin dashboard
|
|
87
|
+
const adminDashboard = `import { db } from '@/lib/db';
|
|
88
|
+
import { Users, CreditCard, DollarSign, TrendingUp } from 'lucide-react';
|
|
89
|
+
|
|
90
|
+
async function getStats() {
|
|
91
|
+
const [totalUsers, activeSubscriptions, totalRevenue] = await Promise.all([
|
|
92
|
+
db.user.count(),
|
|
93
|
+
db.subscription.count({ where: { status: 'ACTIVE' } }),
|
|
94
|
+
db.payment.aggregate({
|
|
95
|
+
_sum: { amount: true },
|
|
96
|
+
where: { status: 'succeeded' },
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
totalUsers,
|
|
102
|
+
activeSubscriptions,
|
|
103
|
+
totalRevenue: (totalRevenue._sum.amount || 0) / 100, // Convert cents to dollars
|
|
104
|
+
mrr: activeSubscriptions * 29, // Simplified MRR calculation
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default async function AdminDashboard() {
|
|
109
|
+
const stats = await getStats();
|
|
110
|
+
|
|
111
|
+
const cards = [
|
|
112
|
+
{
|
|
113
|
+
title: 'Total Users',
|
|
114
|
+
value: stats.totalUsers.toLocaleString(),
|
|
115
|
+
icon: Users,
|
|
116
|
+
color: 'bg-blue-500',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: 'Active Subscriptions',
|
|
120
|
+
value: stats.activeSubscriptions.toLocaleString(),
|
|
121
|
+
icon: CreditCard,
|
|
122
|
+
color: 'bg-green-500',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
title: 'Total Revenue',
|
|
126
|
+
value: \`$\${stats.totalRevenue.toLocaleString()}\`,
|
|
127
|
+
icon: DollarSign,
|
|
128
|
+
color: 'bg-purple-500',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
title: 'Monthly Recurring Revenue',
|
|
132
|
+
value: \`$\${stats.mrr.toLocaleString()}\`,
|
|
133
|
+
icon: TrendingUp,
|
|
134
|
+
color: 'bg-amber-500',
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-8">Dashboard</h1>
|
|
141
|
+
|
|
142
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
143
|
+
{cards.map((card) => (
|
|
144
|
+
<div
|
|
145
|
+
key={card.title}
|
|
146
|
+
className="bg-white rounded-xl border p-6 shadow-sm"
|
|
147
|
+
>
|
|
148
|
+
<div className="flex items-center justify-between mb-4">
|
|
149
|
+
<span className="text-sm font-medium text-gray-500">{card.title}</span>
|
|
150
|
+
<div className={\`p-2 rounded-lg \${card.color}\`}>
|
|
151
|
+
<card.icon className="h-5 w-5 text-white" />
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<p className="text-3xl font-bold text-gray-900">{card.value}</p>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="bg-white rounded-xl border p-6 shadow-sm">
|
|
160
|
+
<h2 className="text-lg font-semibold mb-4">Quick Actions</h2>
|
|
161
|
+
<div className="grid grid-cols-2 gap-4">
|
|
162
|
+
<a
|
|
163
|
+
href="/admin/users"
|
|
164
|
+
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
|
165
|
+
>
|
|
166
|
+
<Users className="h-6 w-6 mb-2 text-gray-600" />
|
|
167
|
+
<span className="font-medium">Manage Users</span>
|
|
168
|
+
</a>
|
|
169
|
+
<a
|
|
170
|
+
href="/admin/subscriptions"
|
|
171
|
+
className="p-4 border rounded-lg hover:bg-gray-50 transition-colors"
|
|
172
|
+
>
|
|
173
|
+
<CreditCard className="h-6 w-6 mb-2 text-gray-600" />
|
|
174
|
+
<span className="font-medium">View Subscriptions</span>
|
|
175
|
+
</a>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
await writeFile(
|
|
184
|
+
path.join(projectPath, 'src/app/admin/page.tsx'),
|
|
185
|
+
adminDashboard
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Generate users management page
|
|
189
|
+
const usersPage = `import { db } from '@/lib/db';
|
|
190
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
191
|
+
|
|
192
|
+
async function getUsers() {
|
|
193
|
+
return db.user.findMany({
|
|
194
|
+
include: {
|
|
195
|
+
subscription: true,
|
|
196
|
+
},
|
|
197
|
+
orderBy: { createdAt: 'desc' },
|
|
198
|
+
take: 100,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default async function UsersPage() {
|
|
203
|
+
const users = await getUsers();
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div>
|
|
207
|
+
<div className="flex items-center justify-between mb-8">
|
|
208
|
+
<h1 className="text-3xl font-bold text-gray-900">Users</h1>
|
|
209
|
+
<span className="text-sm text-gray-500">{users.length} total users</span>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="bg-white rounded-xl border shadow-sm overflow-hidden">
|
|
213
|
+
<table className="w-full">
|
|
214
|
+
<thead>
|
|
215
|
+
<tr className="border-b bg-gray-50">
|
|
216
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
|
|
217
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Role</th>
|
|
218
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Subscription</th>
|
|
219
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Joined</th>
|
|
220
|
+
</tr>
|
|
221
|
+
</thead>
|
|
222
|
+
<tbody className="divide-y">
|
|
223
|
+
{users.map((user) => (
|
|
224
|
+
<tr key={user.id} className="hover:bg-gray-50">
|
|
225
|
+
<td className="px-6 py-4">
|
|
226
|
+
<div>
|
|
227
|
+
<p className="font-medium text-gray-900">{user.name || 'No name'}</p>
|
|
228
|
+
<p className="text-sm text-gray-500">{user.email}</p>
|
|
229
|
+
</div>
|
|
230
|
+
</td>
|
|
231
|
+
<td className="px-6 py-4">
|
|
232
|
+
<span className={\`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium \${
|
|
233
|
+
user.role === 'ADMIN' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'
|
|
234
|
+
}\`}>
|
|
235
|
+
{user.role}
|
|
236
|
+
</span>
|
|
237
|
+
</td>
|
|
238
|
+
<td className="px-6 py-4">
|
|
239
|
+
{user.subscription ? (
|
|
240
|
+
<span className={\`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium \${
|
|
241
|
+
user.subscription.status === 'ACTIVE' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
242
|
+
}\`}>
|
|
243
|
+
{user.subscription.status}
|
|
244
|
+
</span>
|
|
245
|
+
) : (
|
|
246
|
+
<span className="text-sm text-gray-400">Free</span>
|
|
247
|
+
)}
|
|
248
|
+
</td>
|
|
249
|
+
<td className="px-6 py-4 text-sm text-gray-500">
|
|
250
|
+
{formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
|
|
251
|
+
</td>
|
|
252
|
+
</tr>
|
|
253
|
+
))}
|
|
254
|
+
</tbody>
|
|
255
|
+
</table>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
await writeFile(
|
|
263
|
+
path.join(projectPath, 'src/app/admin/users/page.tsx'),
|
|
264
|
+
usersPage
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Generate subscriptions page
|
|
268
|
+
const subscriptionsPage = `import { db } from '@/lib/db';
|
|
269
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
270
|
+
|
|
271
|
+
async function getSubscriptions() {
|
|
272
|
+
return db.subscription.findMany({
|
|
273
|
+
include: {
|
|
274
|
+
user: {
|
|
275
|
+
select: { email: true, name: true },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
orderBy: { createdAt: 'desc' },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default async function SubscriptionsPage() {
|
|
283
|
+
const subscriptions = await getSubscriptions();
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div>
|
|
287
|
+
<div className="flex items-center justify-between mb-8">
|
|
288
|
+
<h1 className="text-3xl font-bold text-gray-900">Subscriptions</h1>
|
|
289
|
+
<span className="text-sm text-gray-500">{subscriptions.length} total</span>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div className="bg-white rounded-xl border shadow-sm overflow-hidden">
|
|
293
|
+
<table className="w-full">
|
|
294
|
+
<thead>
|
|
295
|
+
<tr className="border-b bg-gray-50">
|
|
296
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
|
|
297
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
298
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Plan</th>
|
|
299
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Renews</th>
|
|
300
|
+
</tr>
|
|
301
|
+
</thead>
|
|
302
|
+
<tbody className="divide-y">
|
|
303
|
+
{subscriptions.map((sub) => (
|
|
304
|
+
<tr key={sub.id} className="hover:bg-gray-50">
|
|
305
|
+
<td className="px-6 py-4">
|
|
306
|
+
<div>
|
|
307
|
+
<p className="font-medium text-gray-900">{sub.user.name || 'No name'}</p>
|
|
308
|
+
<p className="text-sm text-gray-500">{sub.user.email}</p>
|
|
309
|
+
</div>
|
|
310
|
+
</td>
|
|
311
|
+
<td className="px-6 py-4">
|
|
312
|
+
<span className={\`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium \${
|
|
313
|
+
sub.status === 'ACTIVE' ? 'bg-green-100 text-green-800' :
|
|
314
|
+
sub.status === 'CANCELED' ? 'bg-red-100 text-red-800' :
|
|
315
|
+
'bg-gray-100 text-gray-800'
|
|
316
|
+
}\`}>
|
|
317
|
+
{sub.status}
|
|
318
|
+
</span>
|
|
319
|
+
</td>
|
|
320
|
+
<td className="px-6 py-4 text-sm text-gray-600">
|
|
321
|
+
{sub.stripePriceId.includes('enterprise') ? 'Enterprise' : 'Pro'}
|
|
322
|
+
</td>
|
|
323
|
+
<td className="px-6 py-4 text-sm text-gray-500">
|
|
324
|
+
{sub.cancelAtPeriodEnd ? (
|
|
325
|
+
<span className="text-amber-600">Canceling</span>
|
|
326
|
+
) : (
|
|
327
|
+
formatDistanceToNow(new Date(sub.currentPeriodEnd), { addSuffix: true })
|
|
328
|
+
)}
|
|
329
|
+
</td>
|
|
330
|
+
</tr>
|
|
331
|
+
))}
|
|
332
|
+
</tbody>
|
|
333
|
+
</table>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
await writeFile(
|
|
341
|
+
path.join(projectPath, 'src/app/admin/subscriptions/page.tsx'),
|
|
342
|
+
subscriptionsPage
|
|
343
|
+
);
|
|
344
|
+
}
|