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
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Keys Generator - Pro Feature
|
|
3
|
+
* Generates API key management system with rate limiting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, ensureDir } from '../../utils/files.js';
|
|
8
|
+
|
|
9
|
+
export async function generateApiKeys(projectPath) {
|
|
10
|
+
await ensureDir(path.join(projectPath, 'src/app/api/keys'));
|
|
11
|
+
await ensureDir(path.join(projectPath, 'src/app/api/keys/create'));
|
|
12
|
+
await ensureDir(path.join(projectPath, 'src/app/api/keys/revoke'));
|
|
13
|
+
await ensureDir(path.join(projectPath, 'src/app/dashboard/api-keys'));
|
|
14
|
+
|
|
15
|
+
// Generate API key utility
|
|
16
|
+
const apiKeyUtil = `import crypto from 'crypto';
|
|
17
|
+
import { db } from '@/lib/db';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a new API key
|
|
21
|
+
* Format: sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
22
|
+
*/
|
|
23
|
+
export function generateApiKey(): { key: string; hash: string; prefix: string } {
|
|
24
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
25
|
+
let randomPart = '';
|
|
26
|
+
for (let i = 0; i < 32; i++) {
|
|
27
|
+
randomPart += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const key = \`sk_live_\${randomPart}\`;
|
|
31
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
32
|
+
const prefix = key.substring(0, 12); // sk_live_XXXX
|
|
33
|
+
|
|
34
|
+
return { key, hash, prefix };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hash an API key for storage
|
|
39
|
+
*/
|
|
40
|
+
export function hashApiKey(key: string): string {
|
|
41
|
+
return crypto.createHash('sha256').update(key).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate an API key and return the user
|
|
46
|
+
*/
|
|
47
|
+
export async function validateApiKey(key: string) {
|
|
48
|
+
if (!key || !key.startsWith('sk_live_')) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hash = hashApiKey(key);
|
|
53
|
+
|
|
54
|
+
const apiKey = await db.apiKey.findUnique({
|
|
55
|
+
where: { key: hash },
|
|
56
|
+
include: {
|
|
57
|
+
user: {
|
|
58
|
+
select: { id: true, email: true, role: true },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!apiKey) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check expiration
|
|
68
|
+
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Update last used timestamp
|
|
73
|
+
await db.apiKey.update({
|
|
74
|
+
where: { id: apiKey.id },
|
|
75
|
+
data: { lastUsedAt: new Date() },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return apiKey.user;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
await writeFile(
|
|
83
|
+
path.join(projectPath, 'src/lib/api-key-auth.ts'),
|
|
84
|
+
apiKeyUtil
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Generate rate limiting utility (works without Redis, in-memory fallback)
|
|
88
|
+
const rateLimitUtil = `/**
|
|
89
|
+
* Simple in-memory rate limiter
|
|
90
|
+
* For production, consider using Upstash Redis
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
interface RateLimitEntry {
|
|
94
|
+
count: number;
|
|
95
|
+
resetAt: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rateLimitStore = new Map<string, RateLimitEntry>();
|
|
99
|
+
|
|
100
|
+
const RATE_LIMIT = 100; // requests
|
|
101
|
+
const WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
102
|
+
|
|
103
|
+
export async function rateLimit(identifier: string): Promise<{
|
|
104
|
+
success: boolean;
|
|
105
|
+
remaining: number;
|
|
106
|
+
reset: number;
|
|
107
|
+
}> {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const entry = rateLimitStore.get(identifier);
|
|
110
|
+
|
|
111
|
+
if (!entry || now > entry.resetAt) {
|
|
112
|
+
// Create new window
|
|
113
|
+
rateLimitStore.set(identifier, {
|
|
114
|
+
count: 1,
|
|
115
|
+
resetAt: now + WINDOW_MS,
|
|
116
|
+
});
|
|
117
|
+
return { success: true, remaining: RATE_LIMIT - 1, reset: now + WINDOW_MS };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (entry.count >= RATE_LIMIT) {
|
|
121
|
+
return { success: false, remaining: 0, reset: entry.resetAt };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
entry.count++;
|
|
125
|
+
return { success: true, remaining: RATE_LIMIT - entry.count, reset: entry.resetAt };
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
await writeFile(
|
|
130
|
+
path.join(projectPath, 'src/lib/rate-limit.ts'),
|
|
131
|
+
rateLimitUtil
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Generate create API key endpoint
|
|
135
|
+
const createKeyRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
136
|
+
import { auth } from '@/lib/auth';
|
|
137
|
+
import { db } from '@/lib/db';
|
|
138
|
+
import { generateApiKey } from '@/lib/api-key-auth';
|
|
139
|
+
|
|
140
|
+
export async function POST(req: NextRequest) {
|
|
141
|
+
try {
|
|
142
|
+
const session = await auth();
|
|
143
|
+
|
|
144
|
+
if (!session?.user?.id) {
|
|
145
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { name } = await req.json();
|
|
149
|
+
|
|
150
|
+
if (!name || name.length < 1) {
|
|
151
|
+
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check existing keys count (limit to 5)
|
|
155
|
+
const existingCount = await db.apiKey.count({
|
|
156
|
+
where: { userId: session.user.id },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (existingCount >= 5) {
|
|
160
|
+
return NextResponse.json(
|
|
161
|
+
{ error: 'Maximum 5 API keys allowed' },
|
|
162
|
+
{ status: 400 }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const { key, hash, prefix } = generateApiKey();
|
|
167
|
+
|
|
168
|
+
await db.apiKey.create({
|
|
169
|
+
data: {
|
|
170
|
+
userId: session.user.id,
|
|
171
|
+
key: hash,
|
|
172
|
+
keyPrefix: prefix,
|
|
173
|
+
name,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Return the actual key only once - user must save it
|
|
178
|
+
return NextResponse.json({
|
|
179
|
+
key, // Full key - only shown once!
|
|
180
|
+
prefix,
|
|
181
|
+
name,
|
|
182
|
+
message: 'Save this key securely - it will not be shown again.',
|
|
183
|
+
});
|
|
184
|
+
} catch (error: any) {
|
|
185
|
+
console.error('Create API key error:', error);
|
|
186
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
await writeFile(
|
|
192
|
+
path.join(projectPath, 'src/app/api/keys/create/route.ts'),
|
|
193
|
+
createKeyRoute
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Generate revoke API key endpoint
|
|
197
|
+
const revokeKeyRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
198
|
+
import { auth } from '@/lib/auth';
|
|
199
|
+
import { db } from '@/lib/db';
|
|
200
|
+
|
|
201
|
+
export async function POST(req: NextRequest) {
|
|
202
|
+
try {
|
|
203
|
+
const session = await auth();
|
|
204
|
+
|
|
205
|
+
if (!session?.user?.id) {
|
|
206
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { keyId } = await req.json();
|
|
210
|
+
|
|
211
|
+
if (!keyId) {
|
|
212
|
+
return NextResponse.json({ error: 'Key ID is required' }, { status: 400 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Ensure user owns this key
|
|
216
|
+
const apiKey = await db.apiKey.findFirst({
|
|
217
|
+
where: {
|
|
218
|
+
id: keyId,
|
|
219
|
+
userId: session.user.id,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!apiKey) {
|
|
224
|
+
return NextResponse.json({ error: 'API key not found' }, { status: 404 });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await db.apiKey.delete({
|
|
228
|
+
where: { id: keyId },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return NextResponse.json({ success: true });
|
|
232
|
+
} catch (error: any) {
|
|
233
|
+
console.error('Revoke API key error:', error);
|
|
234
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
await writeFile(
|
|
240
|
+
path.join(projectPath, 'src/app/api/keys/revoke/route.ts'),
|
|
241
|
+
revokeKeyRoute
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Generate API keys management page
|
|
245
|
+
const apiKeysPage = `'use client';
|
|
246
|
+
|
|
247
|
+
import { useState, useEffect } from 'react';
|
|
248
|
+
import { Key, Plus, Trash2, Copy, Check, Loader2 } from 'lucide-react';
|
|
249
|
+
|
|
250
|
+
interface ApiKey {
|
|
251
|
+
id: string;
|
|
252
|
+
name: string;
|
|
253
|
+
keyPrefix: string;
|
|
254
|
+
lastUsedAt: string | null;
|
|
255
|
+
createdAt: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default function ApiKeysPage() {
|
|
259
|
+
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
260
|
+
const [loading, setLoading] = useState(true);
|
|
261
|
+
const [creating, setCreating] = useState(false);
|
|
262
|
+
const [newKeyName, setNewKeyName] = useState('');
|
|
263
|
+
const [newKey, setNewKey] = useState<string | null>(null);
|
|
264
|
+
const [copied, setCopied] = useState(false);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
fetchKeys();
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
async function fetchKeys() {
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch('/api/keys');
|
|
273
|
+
if (res.ok) {
|
|
274
|
+
const data = await res.json();
|
|
275
|
+
setKeys(data.keys);
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
setLoading(false);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function createKey() {
|
|
283
|
+
if (!newKeyName.trim()) return;
|
|
284
|
+
|
|
285
|
+
setCreating(true);
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch('/api/keys/create', {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
body: JSON.stringify({ name: newKeyName }),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (res.ok) {
|
|
294
|
+
const data = await res.json();
|
|
295
|
+
setNewKey(data.key);
|
|
296
|
+
setNewKeyName('');
|
|
297
|
+
fetchKeys();
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
setCreating(false);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function revokeKey(keyId: string) {
|
|
305
|
+
if (!confirm('Are you sure you want to revoke this API key?')) return;
|
|
306
|
+
|
|
307
|
+
await fetch('/api/keys/revoke', {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: { 'Content-Type': 'application/json' },
|
|
310
|
+
body: JSON.stringify({ keyId }),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
fetchKeys();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function copyToClipboard(text: string) {
|
|
317
|
+
navigator.clipboard.writeText(text);
|
|
318
|
+
setCopied(true);
|
|
319
|
+
setTimeout(() => setCopied(false), 2000);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (loading) {
|
|
323
|
+
return (
|
|
324
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
325
|
+
<Loader2 className="h-8 w-8 animate-spin text-indigo-600" />
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
332
|
+
<h1 className="text-3xl font-bold mb-2">API Keys</h1>
|
|
333
|
+
<p className="text-gray-600 mb-8">Manage your API keys for programmatic access</p>
|
|
334
|
+
|
|
335
|
+
{/* New key created banner */}
|
|
336
|
+
{newKey && (
|
|
337
|
+
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
|
338
|
+
<p className="text-sm text-amber-800 mb-2 font-medium">
|
|
339
|
+
š Save this API key now - it will not be shown again!
|
|
340
|
+
</p>
|
|
341
|
+
<div className="flex items-center gap-2">
|
|
342
|
+
<code className="flex-1 bg-white border rounded px-3 py-2 text-sm font-mono break-all">
|
|
343
|
+
{newKey}
|
|
344
|
+
</code>
|
|
345
|
+
<button
|
|
346
|
+
onClick={() => copyToClipboard(newKey)}
|
|
347
|
+
className="p-2 hover:bg-amber-100 rounded"
|
|
348
|
+
>
|
|
349
|
+
{copied ? <Check className="h-5 w-5 text-green-600" /> : <Copy className="h-5 w-5" />}
|
|
350
|
+
</button>
|
|
351
|
+
</div>
|
|
352
|
+
<button
|
|
353
|
+
onClick={() => setNewKey(null)}
|
|
354
|
+
className="mt-3 text-sm text-amber-700 hover:underline"
|
|
355
|
+
>
|
|
356
|
+
I have saved the key
|
|
357
|
+
</button>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
{/* Create new key */}
|
|
362
|
+
<div className="bg-white rounded-xl border p-6 mb-8">
|
|
363
|
+
<h2 className="text-lg font-semibold mb-4">Create New API Key</h2>
|
|
364
|
+
<div className="flex gap-3">
|
|
365
|
+
<input
|
|
366
|
+
type="text"
|
|
367
|
+
value={newKeyName}
|
|
368
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
369
|
+
placeholder="Key name (e.g., Production, Development)"
|
|
370
|
+
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
371
|
+
/>
|
|
372
|
+
<button
|
|
373
|
+
onClick={createKey}
|
|
374
|
+
disabled={creating || !newKeyName.trim()}
|
|
375
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 disabled:opacity-50"
|
|
376
|
+
>
|
|
377
|
+
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
|
378
|
+
Create Key
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Existing keys */}
|
|
384
|
+
<div className="bg-white rounded-xl border overflow-hidden">
|
|
385
|
+
<div className="px-6 py-4 border-b">
|
|
386
|
+
<h2 className="text-lg font-semibold">Your API Keys</h2>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
{keys.length === 0 ? (
|
|
390
|
+
<div className="p-6 text-center text-gray-500">
|
|
391
|
+
<Key className="h-12 w-12 mx-auto mb-3 text-gray-300" />
|
|
392
|
+
<p>No API keys yet. Create one above.</p>
|
|
393
|
+
</div>
|
|
394
|
+
) : (
|
|
395
|
+
<ul className="divide-y">
|
|
396
|
+
{keys.map((key) => (
|
|
397
|
+
<li key={key.id} className="px-6 py-4 flex items-center justify-between">
|
|
398
|
+
<div>
|
|
399
|
+
<p className="font-medium">{key.name}</p>
|
|
400
|
+
<p className="text-sm text-gray-500 font-mono">{key.keyPrefix}ā¢ā¢ā¢ā¢ā¢ā¢ā¢ā¢</p>
|
|
401
|
+
<p className="text-xs text-gray-400 mt-1">
|
|
402
|
+
{key.lastUsedAt
|
|
403
|
+
? \`Last used: \${new Date(key.lastUsedAt).toLocaleDateString()}\`
|
|
404
|
+
: 'Never used'}
|
|
405
|
+
</p>
|
|
406
|
+
</div>
|
|
407
|
+
<button
|
|
408
|
+
onClick={() => revokeKey(key.id)}
|
|
409
|
+
className="p-2 text-red-500 hover:bg-red-50 rounded"
|
|
410
|
+
>
|
|
411
|
+
<Trash2 className="h-5 w-5" />
|
|
412
|
+
</button>
|
|
413
|
+
</li>
|
|
414
|
+
))}
|
|
415
|
+
</ul>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
`;
|
|
422
|
+
|
|
423
|
+
await writeFile(
|
|
424
|
+
path.join(projectPath, 'src/app/dashboard/api-keys/page.tsx'),
|
|
425
|
+
apiKeysPage
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Generate list API keys endpoint
|
|
429
|
+
const listKeysRoute = `import { NextResponse } from 'next/server';
|
|
430
|
+
import { auth } from '@/lib/auth';
|
|
431
|
+
import { db } from '@/lib/db';
|
|
432
|
+
|
|
433
|
+
export async function GET() {
|
|
434
|
+
try {
|
|
435
|
+
const session = await auth();
|
|
436
|
+
|
|
437
|
+
if (!session?.user?.id) {
|
|
438
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const keys = await db.apiKey.findMany({
|
|
442
|
+
where: { userId: session.user.id },
|
|
443
|
+
select: {
|
|
444
|
+
id: true,
|
|
445
|
+
name: true,
|
|
446
|
+
keyPrefix: true,
|
|
447
|
+
lastUsedAt: true,
|
|
448
|
+
createdAt: true,
|
|
449
|
+
},
|
|
450
|
+
orderBy: { createdAt: 'desc' },
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return NextResponse.json({ keys });
|
|
454
|
+
} catch (error: any) {
|
|
455
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
await writeFile(
|
|
461
|
+
path.join(projectPath, 'src/app/api/keys/route.ts'),
|
|
462
|
+
listKeysRoute
|
|
463
|
+
);
|
|
464
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full Database Schema Generator - Pro Feature
|
|
3
|
+
* Generates complete Prisma schema with Subscription, Payment, ApiKey models
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, ensureDir } from '../../utils/files.js';
|
|
8
|
+
|
|
9
|
+
export async function generateFullDatabase(projectPath) {
|
|
10
|
+
await ensureDir(path.join(projectPath, 'prisma'));
|
|
11
|
+
|
|
12
|
+
// Generate full Prisma schema
|
|
13
|
+
const schema = `// This is your Prisma schema file
|
|
14
|
+
// Generated by SoloStack Pro
|
|
15
|
+
|
|
16
|
+
generator client {
|
|
17
|
+
provider = "prisma-client-js"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
datasource db {
|
|
21
|
+
provider = "postgresql"
|
|
22
|
+
url = env("DATABASE_URL")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ==========================================
|
|
26
|
+
// User & Authentication Models
|
|
27
|
+
// ==========================================
|
|
28
|
+
|
|
29
|
+
model User {
|
|
30
|
+
id String @id @default(cuid())
|
|
31
|
+
email String @unique
|
|
32
|
+
name String?
|
|
33
|
+
emailVerified DateTime?
|
|
34
|
+
image String?
|
|
35
|
+
password String?
|
|
36
|
+
role Role @default(USER)
|
|
37
|
+
stripeCustomerId String? @unique
|
|
38
|
+
subscription Subscription?
|
|
39
|
+
apiKeys ApiKey[]
|
|
40
|
+
createdAt DateTime @default(now())
|
|
41
|
+
updatedAt DateTime @updatedAt
|
|
42
|
+
|
|
43
|
+
accounts Account[]
|
|
44
|
+
sessions Session[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
model Account {
|
|
48
|
+
userId String
|
|
49
|
+
type String
|
|
50
|
+
provider String
|
|
51
|
+
providerAccountId String
|
|
52
|
+
refresh_token String?
|
|
53
|
+
access_token String?
|
|
54
|
+
expires_at Int?
|
|
55
|
+
token_type String?
|
|
56
|
+
scope String?
|
|
57
|
+
id_token String?
|
|
58
|
+
session_state String?
|
|
59
|
+
|
|
60
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
61
|
+
|
|
62
|
+
@@id([provider, providerAccountId])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
model Session {
|
|
66
|
+
sessionToken String @unique
|
|
67
|
+
userId String
|
|
68
|
+
expires DateTime
|
|
69
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
model VerificationToken {
|
|
73
|
+
identifier String
|
|
74
|
+
token String @unique
|
|
75
|
+
expires DateTime
|
|
76
|
+
|
|
77
|
+
@@unique([identifier, token])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ==========================================
|
|
81
|
+
// Subscription & Payments Models
|
|
82
|
+
// ==========================================
|
|
83
|
+
|
|
84
|
+
model Subscription {
|
|
85
|
+
id String @id @default(cuid())
|
|
86
|
+
userId String @unique
|
|
87
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
88
|
+
stripeSubscriptionId String @unique
|
|
89
|
+
stripePriceId String
|
|
90
|
+
status SubscriptionStatus
|
|
91
|
+
currentPeriodStart DateTime
|
|
92
|
+
currentPeriodEnd DateTime
|
|
93
|
+
cancelAtPeriodEnd Boolean @default(false)
|
|
94
|
+
createdAt DateTime @default(now())
|
|
95
|
+
updatedAt DateTime @updatedAt
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
model Payment {
|
|
99
|
+
id String @id @default(cuid())
|
|
100
|
+
userId String
|
|
101
|
+
stripePaymentId String @unique
|
|
102
|
+
amount Int // Amount in cents
|
|
103
|
+
currency String @default("usd")
|
|
104
|
+
status String
|
|
105
|
+
createdAt DateTime @default(now())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
model StripeEvent {
|
|
109
|
+
id String @id
|
|
110
|
+
type String
|
|
111
|
+
processed Boolean @default(false)
|
|
112
|
+
createdAt DateTime @default(now())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ==========================================
|
|
116
|
+
// API Key Model
|
|
117
|
+
// ==========================================
|
|
118
|
+
|
|
119
|
+
model ApiKey {
|
|
120
|
+
id String @id @default(cuid())
|
|
121
|
+
userId String
|
|
122
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
123
|
+
key String @unique // Hashed key
|
|
124
|
+
keyPrefix String // First 8 chars for display (sk_live_abc1...)
|
|
125
|
+
name String
|
|
126
|
+
lastUsedAt DateTime?
|
|
127
|
+
createdAt DateTime @default(now())
|
|
128
|
+
expiresAt DateTime?
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ==========================================
|
|
132
|
+
// Enums
|
|
133
|
+
// ==========================================
|
|
134
|
+
|
|
135
|
+
enum Role {
|
|
136
|
+
USER
|
|
137
|
+
ADMIN
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
enum SubscriptionStatus {
|
|
141
|
+
ACTIVE
|
|
142
|
+
CANCELED
|
|
143
|
+
PAST_DUE
|
|
144
|
+
TRIALING
|
|
145
|
+
INCOMPLETE
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
await writeFile(path.join(projectPath, 'prisma/schema.prisma'), schema);
|
|
150
|
+
|
|
151
|
+
// Generate seed script
|
|
152
|
+
const seed = `import { PrismaClient } from '@prisma/client';
|
|
153
|
+
import bcrypt from 'bcryptjs';
|
|
154
|
+
|
|
155
|
+
const prisma = new PrismaClient();
|
|
156
|
+
|
|
157
|
+
async function main() {
|
|
158
|
+
console.log('š± Seeding database...');
|
|
159
|
+
|
|
160
|
+
// Create admin user
|
|
161
|
+
const adminPassword = await bcrypt.hash('admin123', 12);
|
|
162
|
+
const admin = await prisma.user.upsert({
|
|
163
|
+
where: { email: 'admin@example.com' },
|
|
164
|
+
update: {},
|
|
165
|
+
create: {
|
|
166
|
+
email: 'admin@example.com',
|
|
167
|
+
name: 'Admin User',
|
|
168
|
+
password: adminPassword,
|
|
169
|
+
role: 'ADMIN',
|
|
170
|
+
emailVerified: new Date(),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
console.log('ā Created admin user:', admin.email);
|
|
174
|
+
|
|
175
|
+
// Create test user with subscription
|
|
176
|
+
const userPassword = await bcrypt.hash('user123', 12);
|
|
177
|
+
const user = await prisma.user.upsert({
|
|
178
|
+
where: { email: 'user@example.com' },
|
|
179
|
+
update: {},
|
|
180
|
+
create: {
|
|
181
|
+
email: 'user@example.com',
|
|
182
|
+
name: 'Test User',
|
|
183
|
+
password: userPassword,
|
|
184
|
+
role: 'USER',
|
|
185
|
+
emailVerified: new Date(),
|
|
186
|
+
stripeCustomerId: 'cus_test123',
|
|
187
|
+
subscription: {
|
|
188
|
+
create: {
|
|
189
|
+
stripeSubscriptionId: 'sub_test123',
|
|
190
|
+
stripePriceId: 'price_pro',
|
|
191
|
+
status: 'ACTIVE',
|
|
192
|
+
currentPeriodStart: new Date(),
|
|
193
|
+
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
console.log('ā Created test user with subscription:', user.email);
|
|
199
|
+
|
|
200
|
+
// Create free user
|
|
201
|
+
const freePassword = await bcrypt.hash('free123', 12);
|
|
202
|
+
const freeUser = await prisma.user.upsert({
|
|
203
|
+
where: { email: 'free@example.com' },
|
|
204
|
+
update: {},
|
|
205
|
+
create: {
|
|
206
|
+
email: 'free@example.com',
|
|
207
|
+
name: 'Free User',
|
|
208
|
+
password: freePassword,
|
|
209
|
+
role: 'USER',
|
|
210
|
+
emailVerified: new Date(),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
console.log('ā Created free user:', freeUser.email);
|
|
214
|
+
|
|
215
|
+
console.log('\\nā
Seeding complete!');
|
|
216
|
+
console.log('\\nš Test credentials:');
|
|
217
|
+
console.log(' Admin: admin@example.com / admin123');
|
|
218
|
+
console.log(' Pro User: user@example.com / user123');
|
|
219
|
+
console.log(' Free User: free@example.com / free123');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main()
|
|
223
|
+
.catch((e) => {
|
|
224
|
+
console.error(e);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
})
|
|
227
|
+
.finally(async () => {
|
|
228
|
+
await prisma.$disconnect();
|
|
229
|
+
});
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
await writeFile(path.join(projectPath, 'prisma/seed.ts'), seed);
|
|
233
|
+
}
|