create-solostack 1.3.4 → 1.3.6
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/auth.js +234 -24
- package/src/index.js +1 -1
package/package.json
CHANGED
package/src/generators/auth.js
CHANGED
|
@@ -21,6 +21,7 @@ async function generateSupabaseAuth(projectPath) {
|
|
|
21
21
|
// Create auth directories
|
|
22
22
|
await ensureDir(path.join(projectPath, 'src/app/auth/callback'));
|
|
23
23
|
await ensureDir(path.join(projectPath, 'src/app/login'));
|
|
24
|
+
await ensureDir(path.join(projectPath, 'src/app/signup'));
|
|
24
25
|
await ensureDir(path.join(projectPath, 'src/app/private'));
|
|
25
26
|
|
|
26
27
|
// 1. Client Utility
|
|
@@ -35,35 +36,29 @@ export function createClient() {
|
|
|
35
36
|
`;
|
|
36
37
|
await writeFile(path.join(projectPath, 'src/utils/supabase/client.ts'), clientUtil);
|
|
37
38
|
|
|
38
|
-
// 2. Server Utility
|
|
39
|
-
const serverUtil = `import { createServerClient
|
|
39
|
+
// 2. Server Utility (Next.js 15 compatible - async cookies)
|
|
40
|
+
const serverUtil = `import { createServerClient } from '@supabase/ssr'
|
|
40
41
|
import { cookies } from 'next/headers'
|
|
41
42
|
|
|
42
|
-
export function createClient(
|
|
43
|
+
export async function createClient() {
|
|
44
|
+
const cookieStore = await cookies()
|
|
45
|
+
|
|
43
46
|
return createServerClient(
|
|
44
47
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
45
48
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
46
49
|
{
|
|
47
50
|
cookies: {
|
|
48
|
-
|
|
49
|
-
return cookieStore.
|
|
50
|
-
},
|
|
51
|
-
set(name: string, value: string, options: CookieOptions) {
|
|
52
|
-
try {
|
|
53
|
-
cookieStore.set({ name, value, ...options })
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// The \`set\` method was called from a Server Component.
|
|
56
|
-
// This can be ignored if you have middleware refreshing
|
|
57
|
-
// user sessions.
|
|
58
|
-
}
|
|
51
|
+
getAll() {
|
|
52
|
+
return cookieStore.getAll()
|
|
59
53
|
},
|
|
60
|
-
|
|
54
|
+
setAll(cookiesToSet) {
|
|
61
55
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
56
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
57
|
+
cookieStore.set(name, value, options)
|
|
58
|
+
)
|
|
59
|
+
} catch {
|
|
60
|
+
// The \`setAll\` method was called from a Server Component.
|
|
61
|
+
// This can be ignored if you have middleware refreshing user sessions.
|
|
67
62
|
}
|
|
68
63
|
},
|
|
69
64
|
},
|
|
@@ -73,7 +68,7 @@ export function createClient(cookieStore: ReturnType<typeof cookies>) {
|
|
|
73
68
|
`;
|
|
74
69
|
await writeFile(path.join(projectPath, 'src/utils/supabase/server.ts'), serverUtil);
|
|
75
70
|
|
|
76
|
-
// 3.
|
|
71
|
+
// 3. Middleware Utility
|
|
77
72
|
const middlewareUtil = `import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
|
78
73
|
import { NextResponse, type NextRequest } from 'next/server'
|
|
79
74
|
|
|
@@ -162,7 +157,6 @@ export const config = {
|
|
|
162
157
|
|
|
163
158
|
// 5. Auth Callback Route
|
|
164
159
|
const routeCallback = `import { NextResponse } from 'next/server'
|
|
165
|
-
import { cookies } from 'next/headers'
|
|
166
160
|
import { createClient } from '@/utils/supabase/server'
|
|
167
161
|
|
|
168
162
|
export async function GET(request: Request) {
|
|
@@ -171,8 +165,7 @@ export async function GET(request: Request) {
|
|
|
171
165
|
const next = searchParams.get('next') ?? '/'
|
|
172
166
|
|
|
173
167
|
if (code) {
|
|
174
|
-
const
|
|
175
|
-
const supabase = createClient(cookieStore)
|
|
168
|
+
const supabase = await createClient()
|
|
176
169
|
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
|
177
170
|
if (!error) {
|
|
178
171
|
return NextResponse.redirect(new URL(next, request.url))
|
|
@@ -191,6 +184,7 @@ import { createClient } from '@/utils/supabase/client';
|
|
|
191
184
|
import { useRouter } from 'next/navigation';
|
|
192
185
|
import { useState } from 'react';
|
|
193
186
|
import { Rocket } from 'lucide-react';
|
|
187
|
+
import Link from 'next/link';
|
|
194
188
|
|
|
195
189
|
export default function LoginPage() {
|
|
196
190
|
const router = useRouter();
|
|
@@ -301,6 +295,13 @@ export default function LoginPage() {
|
|
|
301
295
|
{loading ? 'Signing in...' : 'Sign In'}
|
|
302
296
|
</button>
|
|
303
297
|
</form>
|
|
298
|
+
|
|
299
|
+
<div className="text-center text-sm">
|
|
300
|
+
<span className="text-zinc-500">Don't have an account? </span>
|
|
301
|
+
<Link href="/signup" className="font-medium text-indigo-400 hover:text-indigo-300">
|
|
302
|
+
Sign up
|
|
303
|
+
</Link>
|
|
304
|
+
</div>
|
|
304
305
|
</div>
|
|
305
306
|
</div>
|
|
306
307
|
);
|
|
@@ -308,6 +309,215 @@ export default function LoginPage() {
|
|
|
308
309
|
`;
|
|
309
310
|
|
|
310
311
|
await writeFile(path.join(projectPath, 'src/app/login/page.tsx'), loginPage);
|
|
312
|
+
|
|
313
|
+
// 7. Signup Page (Supabase)
|
|
314
|
+
const signupPage = `'use client';
|
|
315
|
+
import { createClient } from '@/utils/supabase/client';
|
|
316
|
+
import { useRouter } from 'next/navigation';
|
|
317
|
+
import { useState } from 'react';
|
|
318
|
+
import { Rocket } from 'lucide-react';
|
|
319
|
+
import Link from 'next/link';
|
|
320
|
+
|
|
321
|
+
export default function SignupPage() {
|
|
322
|
+
const router = useRouter();
|
|
323
|
+
const [email, setEmail] = useState('');
|
|
324
|
+
const [password, setPassword] = useState('');
|
|
325
|
+
const [loading, setLoading] = useState(false);
|
|
326
|
+
const [message, setMessage] = useState('');
|
|
327
|
+
|
|
328
|
+
const supabase = createClient();
|
|
329
|
+
|
|
330
|
+
const handleEmailSignup = async (e: React.FormEvent) => {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
setLoading(true);
|
|
333
|
+
setMessage('');
|
|
334
|
+
|
|
335
|
+
const { error } = await supabase.auth.signUp({
|
|
336
|
+
email,
|
|
337
|
+
password,
|
|
338
|
+
options: {
|
|
339
|
+
emailRedirectTo: \`\${location.origin}/auth/callback\`,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (error) {
|
|
344
|
+
setMessage(error.message);
|
|
345
|
+
} else {
|
|
346
|
+
setMessage('Check your email for the confirmation link!');
|
|
347
|
+
}
|
|
348
|
+
setLoading(false);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const handleGitHubLogin = async () => {
|
|
352
|
+
await supabase.auth.signInWithOAuth({
|
|
353
|
+
provider: 'github',
|
|
354
|
+
options: {
|
|
355
|
+
redirectTo: \`\${location.origin}/auth/callback\`,
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const handleGoogleLogin = async () => {
|
|
361
|
+
await supabase.auth.signInWithOAuth({
|
|
362
|
+
provider: 'google',
|
|
363
|
+
options: {
|
|
364
|
+
redirectTo: \`\${location.origin}/auth/callback\`,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-zinc-950 p-4 text-white">
|
|
371
|
+
<div className="w-full max-w-sm space-y-8 rounded-xl border border-zinc-800 bg-zinc-900/50 p-8 shadow-xl backdrop-blur-xl">
|
|
372
|
+
<div className="text-center">
|
|
373
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-600">
|
|
374
|
+
<Rocket className="h-6 w-6 text-white" />
|
|
375
|
+
</div>
|
|
376
|
+
<h2 className="text-2xl font-bold tracking-tight">Create account</h2>
|
|
377
|
+
<p className="mt-2 text-sm text-zinc-400">Sign up to get started</p>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div className="space-y-4">
|
|
381
|
+
<button
|
|
382
|
+
onClick={handleGitHubLogin}
|
|
383
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-800 py-2 text-sm font-medium hover:bg-zinc-700 transition-colors"
|
|
384
|
+
>
|
|
385
|
+
Continue with GitHub
|
|
386
|
+
</button>
|
|
387
|
+
<button
|
|
388
|
+
onClick={handleGoogleLogin}
|
|
389
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-800 py-2 text-sm font-medium hover:bg-zinc-700 transition-colors"
|
|
390
|
+
>
|
|
391
|
+
Continue with Google
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div className="relative">
|
|
396
|
+
<div className="absolute inset-0 flex items-center">
|
|
397
|
+
<div className="w-full border-t border-zinc-800" />
|
|
398
|
+
</div>
|
|
399
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
400
|
+
<span className="bg-zinc-900 px-2 text-zinc-500">Or continue with</span>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<form onSubmit={handleEmailSignup} className="space-y-4">
|
|
405
|
+
<div>
|
|
406
|
+
<label className="mb-2 block text-sm font-medium text-zinc-400">Email</label>
|
|
407
|
+
<input
|
|
408
|
+
type="email"
|
|
409
|
+
value={email}
|
|
410
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
411
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm placeholder:text-zinc-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
412
|
+
required
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
<div>
|
|
416
|
+
<label className="mb-2 block text-sm font-medium text-zinc-400">Password</label>
|
|
417
|
+
<input
|
|
418
|
+
type="password"
|
|
419
|
+
value={password}
|
|
420
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
421
|
+
className="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm placeholder:text-zinc-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
422
|
+
placeholder="Min 6 characters"
|
|
423
|
+
minLength={6}
|
|
424
|
+
required
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
{message && (
|
|
429
|
+
<p className={message.includes('Check') ? 'text-sm text-green-400' : 'text-sm text-red-400'}>
|
|
430
|
+
{message}
|
|
431
|
+
</p>
|
|
432
|
+
)}
|
|
433
|
+
|
|
434
|
+
<button
|
|
435
|
+
type="submit"
|
|
436
|
+
disabled={loading}
|
|
437
|
+
className="w-full rounded-md bg-indigo-600 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
438
|
+
>
|
|
439
|
+
{loading ? 'Creating account...' : 'Sign Up'}
|
|
440
|
+
</button>
|
|
441
|
+
</form>
|
|
442
|
+
|
|
443
|
+
<div className="text-center text-sm">
|
|
444
|
+
<span className="text-zinc-500">Already have an account? </span>
|
|
445
|
+
<Link href="/login" className="font-medium text-indigo-400 hover:text-indigo-300">
|
|
446
|
+
Sign in
|
|
447
|
+
</Link>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
`;
|
|
454
|
+
|
|
455
|
+
await writeFile(path.join(projectPath, 'src/app/signup/page.tsx'), signupPage);
|
|
456
|
+
|
|
457
|
+
// 8. Dashboard Layout (Supabase)
|
|
458
|
+
await ensureDir(path.join(projectPath, 'src/app/dashboard'));
|
|
459
|
+
|
|
460
|
+
const dashboardLayout = `import { redirect } from 'next/navigation';
|
|
461
|
+
import Link from 'next/link';
|
|
462
|
+
import { createClient } from '@/utils/supabase/server';
|
|
463
|
+
|
|
464
|
+
export default async function DashboardLayout({
|
|
465
|
+
children,
|
|
466
|
+
}: {
|
|
467
|
+
children: React.ReactNode;
|
|
468
|
+
}) {
|
|
469
|
+
const supabase = await createClient();
|
|
470
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
471
|
+
|
|
472
|
+
if (!user) {
|
|
473
|
+
redirect('/login');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<div className="min-h-screen bg-gray-50">
|
|
478
|
+
<nav className="bg-white border-b">
|
|
479
|
+
<div className="container mx-auto px-4 py-4">
|
|
480
|
+
<div className="flex items-center justify-between">
|
|
481
|
+
<Link href="/dashboard" className="text-xl font-bold">
|
|
482
|
+
Dashboard
|
|
483
|
+
</Link>
|
|
484
|
+
<div className="flex items-center gap-6">
|
|
485
|
+
<Link href="/dashboard" className="text-sm hover:text-indigo-600">
|
|
486
|
+
Home
|
|
487
|
+
</Link>
|
|
488
|
+
<Link href="/dashboard/billing" className="text-sm hover:text-indigo-600">
|
|
489
|
+
Billing
|
|
490
|
+
</Link>
|
|
491
|
+
<Link href="/dashboard/settings" className="text-sm hover:text-indigo-600">
|
|
492
|
+
Settings
|
|
493
|
+
</Link>
|
|
494
|
+
<Link href="/api/auth/signout" className="text-sm text-red-600 hover:text-red-700">
|
|
495
|
+
Sign Out
|
|
496
|
+
</Link>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</nav>
|
|
501
|
+
<main>{children}</main>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
`;
|
|
506
|
+
await writeFile(path.join(projectPath, 'src/app/dashboard/layout.tsx'), dashboardLayout);
|
|
507
|
+
|
|
508
|
+
// 9. Signout Route (Supabase)
|
|
509
|
+
await ensureDir(path.join(projectPath, 'src/app/api/auth/signout'));
|
|
510
|
+
|
|
511
|
+
const signoutRoute = `import { NextResponse } from 'next/server';
|
|
512
|
+
import { createClient } from '@/utils/supabase/server';
|
|
513
|
+
|
|
514
|
+
export async function GET() {
|
|
515
|
+
const supabase = await createClient();
|
|
516
|
+
await supabase.auth.signOut();
|
|
517
|
+
return NextResponse.redirect(new URL('/login', process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'));
|
|
518
|
+
}
|
|
519
|
+
`;
|
|
520
|
+
await writeFile(path.join(projectPath, 'src/app/api/auth/signout/route.ts'), signoutRoute);
|
|
311
521
|
}
|
|
312
522
|
|
|
313
523
|
async function generateNextAuth(projectPath) {
|
package/src/index.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function main() {
|
|
|
53
53
|
╚════════════════════════════════════════════════════════════════╝
|
|
54
54
|
`));
|
|
55
55
|
|
|
56
|
-
console.log(chalk.gray(` Version ${orangeBright('1.3.
|
|
56
|
+
console.log(chalk.gray(` Version ${orangeBright('1.3.6')} • Built by ${orangeBright('Danish Akhtar')} • ${orangeBright('github.com/danish296')}\n`));
|
|
57
57
|
|
|
58
58
|
// Parse command line arguments
|
|
59
59
|
program
|