stackpatch 1.1.4 → 1.1.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.
Files changed (37) hide show
  1. package/README.md +76 -69
  2. package/bin/stackpatch.js +79 -0
  3. package/bin/stackpatch.ts +2445 -3
  4. package/boilerplate/auth/app/api/auth/[...nextauth]/route.ts +124 -0
  5. package/boilerplate/auth/app/api/auth/signup/route.ts +45 -0
  6. package/boilerplate/auth/app/auth/login/page.tsx +24 -50
  7. package/boilerplate/auth/app/auth/signup/page.tsx +56 -69
  8. package/boilerplate/auth/app/dashboard/page.tsx +82 -0
  9. package/boilerplate/auth/app/login/page.tsx +136 -0
  10. package/boilerplate/auth/app/page.tsx +48 -0
  11. package/boilerplate/auth/components/auth-button.tsx +43 -0
  12. package/boilerplate/auth/components/auth-navbar.tsx +118 -0
  13. package/boilerplate/auth/components/protected-route.tsx +74 -0
  14. package/boilerplate/auth/components/session-provider.tsx +11 -0
  15. package/boilerplate/auth/middleware.ts +51 -0
  16. package/package.json +5 -6
  17. package/boilerplate/auth/app/stackpatch/page.tsx +0 -269
  18. package/boilerplate/auth/components/auth-wrapper.tsx +0 -61
  19. package/src/auth/generator.ts +0 -569
  20. package/src/auth/index.ts +0 -372
  21. package/src/auth/setup.ts +0 -293
  22. package/src/commands/add.ts +0 -112
  23. package/src/commands/create.ts +0 -128
  24. package/src/commands/revert.ts +0 -389
  25. package/src/config.ts +0 -52
  26. package/src/fileOps/copy.ts +0 -224
  27. package/src/fileOps/layout.ts +0 -304
  28. package/src/fileOps/protected.ts +0 -67
  29. package/src/index.ts +0 -215
  30. package/src/manifest.ts +0 -87
  31. package/src/ui/logo.ts +0 -24
  32. package/src/ui/progress.ts +0 -82
  33. package/src/utils/dependencies.ts +0 -114
  34. package/src/utils/deps-check.ts +0 -45
  35. package/src/utils/files.ts +0 -58
  36. package/src/utils/paths.ts +0 -217
  37. package/src/utils/scanner.ts +0 -109
@@ -1,61 +0,0 @@
1
- "use client";
2
-
3
- import { ReactNode, useEffect } from "react";
4
- import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
- import { authClient } from "@/lib/auth-client";
6
- import { isProtectedRoute } from "@/lib/protected-routes";
7
-
8
- /**
9
- * Auth Wrapper Component
10
- *
11
- * This wrapper checks authentication for protected routes and handles redirects.
12
- * It works alongside middleware to ensure routes are properly protected.
13
- */
14
- export function AuthWrapper({ children }: { children: ReactNode }) {
15
- const pathname = usePathname();
16
- const router = useRouter();
17
- const searchParams = useSearchParams();
18
- const { data: session, isPending } = authClient.useSession();
19
-
20
- useEffect(() => {
21
- // Don't do anything while loading
22
- if (isPending) return;
23
-
24
- // Handle auth pages (login/signup)
25
- if (pathname === "/auth/login" || pathname === "/auth/signup") {
26
- if (session?.user) {
27
- // Already authenticated - redirect away from auth pages
28
- const redirectParam = searchParams.get("redirect");
29
- const redirectTo = redirectParam || "/stackpatch";
30
- router.push(redirectTo);
31
- }
32
- return; // Allow access to auth pages if not authenticated
33
- }
34
-
35
- // Handle protected routes
36
- if (isProtectedRoute(pathname)) {
37
- if (!session?.user) {
38
- // Not authenticated - redirect to login with return URL
39
- const loginUrl = `/auth/login?redirect=${encodeURIComponent(pathname)}`;
40
- router.push(loginUrl);
41
- }
42
- return; // Allow access if authenticated
43
- }
44
-
45
- // For all other routes, allow access
46
- }, [pathname, session, isPending, router, searchParams]);
47
-
48
- // Show loading state while checking session (only on protected routes)
49
- if (isPending && isProtectedRoute(pathname) && !session?.user) {
50
- return (
51
- <div className="flex min-h-screen items-center justify-center">
52
- <div className="text-center">
53
- <div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
54
- <p className="mt-4 text-sm text-zinc-600 dark:text-zinc-400">Loading...</p>
55
- </div>
56
- </div>
57
- );
58
- }
59
-
60
- return <>{children}</>;
61
- }
@@ -1,569 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import chalk from "chalk";
4
- import { detectAppDirectory, detectComponentsDirectory } from "../utils/paths.js";
5
- import type { AuthConfig } from "./setup.js";
6
- import type { ProjectScan } from "../utils/scanner.js";
7
-
8
- /**
9
- * Generate Better Auth files based on configuration
10
- */
11
-
12
- /**
13
- * Generate auth instance file
14
- */
15
- export function generateAuthInstance(
16
- target: string,
17
- config: AuthConfig,
18
- scan: ProjectScan
19
- ): string {
20
- // Determine auth file location
21
- const libDir = scan.hasSrcDir
22
- ? path.join(target, "src", "lib")
23
- : path.join(target, "lib");
24
- const authPath = path.join(libDir, "auth.ts");
25
-
26
- // Create lib directory if it doesn't exist
27
- if (!fs.existsSync(libDir)) {
28
- fs.mkdirSync(libDir, { recursive: true });
29
- }
30
-
31
- // CRITICAL: If file exists, delete it first to ensure clean generation
32
- // This prevents any leftover database imports from previous runs
33
- if (fs.existsSync(authPath)) {
34
- fs.unlinkSync(authPath);
35
- }
36
-
37
- // Always overwrite the auth.ts file to ensure it matches the current configuration
38
- // This prevents old database code from persisting when switching to stateless mode
39
-
40
- // VALIDATION: Ensure config is valid for stateless mode
41
- // If sessionMode is "stateless", force database and orm to "none"
42
- if (config.sessionMode === "stateless") {
43
- config.database = "none";
44
- config.orm = "none";
45
- }
46
-
47
- let imports = 'import { betterAuth } from "better-auth";\n';
48
- imports += 'import { nextCookies } from "better-auth/next-js";\n';
49
- let databaseConfig = "";
50
- let sessionConfig = "";
51
- let accountConfig = "";
52
-
53
- // Stateless session configuration (when no database)
54
- // IMPORTANT: If stateless mode, skip ALL database code generation
55
- // CRITICAL: Check sessionMode first - if it's "stateless", NEVER generate database code
56
- const isStateless = config.sessionMode === "stateless" || config.database === "none";
57
-
58
- // DEFENSIVE: If stateless mode is explicitly selected, force stateless regardless of other config
59
- if (config.sessionMode === "stateless") {
60
- sessionConfig = ` session: {
61
- cookieCache: {
62
- enabled: true,
63
- maxAge: 7 * 24 * 60 * 60, // 7 days cache duration
64
- strategy: "jwe", // can be "jwt" or "compact"
65
- refreshCache: true, // Enable stateless refresh
66
- },
67
- },`;
68
- accountConfig = ` account: {
69
- storeStateStrategy: "cookie",
70
- storeAccountCookie: true, // Store account data after OAuth flow in a cookie
71
- },`;
72
- // EXPLICITLY skip all database code generation - do not proceed to database config
73
- } else if (isStateless) {
74
- // Fallback: if database is "none" but sessionMode is "database", still use stateless
75
- sessionConfig = ` session: {
76
- cookieCache: {
77
- enabled: true,
78
- maxAge: 7 * 24 * 60 * 60, // 7 days cache duration
79
- strategy: "jwe", // can be "jwt" or "compact"
80
- refreshCache: true, // Enable stateless refresh
81
- },
82
- },`;
83
- accountConfig = ` account: {
84
- storeStateStrategy: "cookie",
85
- storeAccountCookie: true, // Store account data after OAuth flow in a cookie
86
- },`;
87
- }
88
-
89
- // Database configuration - ONLY generate if database mode is selected AND database is not "none"
90
- // CRITICAL: Double-check that we're NOT in stateless mode before generating ANY database code
91
- // This is a defensive check to prevent database imports in stateless mode
92
- if (
93
- !isStateless &&
94
- config.sessionMode === "database" &&
95
- config.database !== "none" &&
96
- config.orm !== "none"
97
- ) {
98
- if (config.orm === "drizzle") {
99
- imports += 'import { drizzleAdapter } from "better-auth/adapters/drizzle";\n';
100
- imports += 'import { db } from "@/db"; // Your drizzle instance\n';
101
- databaseConfig = ` database: drizzleAdapter(db, {
102
- provider: "${config.database === "postgres" ? "pg" : config.database}",
103
- }),`;
104
- } else if (config.orm === "prisma") {
105
- imports += 'import { prismaAdapter } from "better-auth/adapters/prisma";\n';
106
- imports += 'import { prisma } from "@/lib/prisma"; // Your prisma instance\n';
107
- databaseConfig = ` database: prismaAdapter(prisma),`;
108
- } else if (config.orm === "raw") {
109
- if (config.database === "sqlite") {
110
- imports += 'import Database from "better-sqlite3";\n';
111
- databaseConfig = ` database: new Database(process.env.DATABASE_URL || "./sqlite.db"),`;
112
- } else if (config.database === "postgres") {
113
- imports += 'import { PostgresJsAdapter } from "better-auth/adapters/postgres";\n';
114
- imports += 'import postgres from "postgres";\n';
115
- imports += 'const sql = postgres(process.env.DATABASE_URL!);\n';
116
- databaseConfig = ` database: new PostgresJsAdapter(sql),`;
117
- } else if (config.database === "mysql") {
118
- imports += 'import { MySqlAdapter } from "better-auth/adapters/mysql";\n';
119
- imports += 'import mysql from "mysql2/promise";\n';
120
- imports += 'const connection = await mysql.createConnection(process.env.DATABASE_URL!);\n';
121
- databaseConfig = ` database: new MySqlAdapter(connection),`;
122
- }
123
- }
124
- }
125
-
126
- // Email and password config
127
- // Email/password works in both database and stateless modes
128
- let emailPasswordConfig = "";
129
- if (config.emailPassword) {
130
- emailPasswordConfig = ` emailAndPassword: {
131
- enabled: true,
132
- },`;
133
- }
134
-
135
- // Social providers config
136
- let socialProvidersConfig = "";
137
- const socialProviders: string[] = [];
138
- if (config.oauthProviders.includes("google")) {
139
- socialProviders.push(` google: {
140
- clientId: process.env.GOOGLE_CLIENT_ID as string,
141
- clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
142
- }`);
143
- }
144
- if (config.oauthProviders.includes("github")) {
145
- socialProviders.push(` github: {
146
- clientId: process.env.GITHUB_CLIENT_ID as string,
147
- clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
148
- }`);
149
- }
150
-
151
- if (socialProviders.length > 0) {
152
- socialProvidersConfig = ` socialProviders: {
153
- ${socialProviders.join(",\n")}
154
- },`;
155
- }
156
-
157
- // Build the auth configuration
158
- // CRITICAL: In stateless mode, NEVER include databaseConfig, even if it was somehow set
159
- const configParts: string[] = [];
160
- if (!isStateless && databaseConfig) {
161
- configParts.push(databaseConfig);
162
- }
163
- if (sessionConfig) configParts.push(sessionConfig);
164
- if (accountConfig) configParts.push(accountConfig);
165
- if (emailPasswordConfig) configParts.push(emailPasswordConfig);
166
- if (socialProvidersConfig) configParts.push(socialProvidersConfig);
167
-
168
- // Always add nextCookies plugin for Next.js integration
169
- configParts.push(` plugins: [nextCookies()],`);
170
-
171
- // Final safety check: Remove any database imports if in stateless mode
172
- let finalImports = imports;
173
- if (isStateless) {
174
- // Remove all database-related imports
175
- finalImports = finalImports
176
- .replace(/import Database from "better-sqlite3";\n?/g, "")
177
- .replace(/import { drizzleAdapter } from "better-auth\/adapters\/drizzle";\n?/g, "")
178
- .replace(/import { prismaAdapter } from "better-auth\/adapters\/prisma";\n?/g, "")
179
- .replace(/import { PostgresJsAdapter } from "better-auth\/adapters\/postgres";\n?/g, "")
180
- .replace(/import { MySqlAdapter } from "better-auth\/adapters\/mysql";\n?/g, "")
181
- .replace(/import postgres from "postgres";\n?/g, "")
182
- .replace(/import mysql from "mysql2\/promise";\n?/g, "")
183
- .replace(/import { db } from "@\/db";.*\n?/g, "")
184
- .replace(/import { prisma } from "@\/lib\/prisma";.*\n?/g, "")
185
- .replace(/const sql = postgres\(process\.env\.DATABASE_URL!\);\n?/g, "")
186
- .replace(/const connection = await mysql\.createConnection\(process\.env\.DATABASE_URL!\);\n?/g, "");
187
- }
188
-
189
- const authContent = `${finalImports}
190
- export const auth = betterAuth({
191
- ${configParts.join("\n")}});
192
- `;
193
-
194
- // Always overwrite the file to ensure clean state
195
- fs.writeFileSync(authPath, authContent, "utf-8");
196
- return path.relative(target, authPath);
197
- }
198
-
199
- /**
200
- * Generate auth client file
201
- */
202
- export function generateAuthClient(target: string, scan: ProjectScan): string {
203
- const libDir = scan.hasSrcDir
204
- ? path.join(target, "src", "lib")
205
- : path.join(target, "lib");
206
- const clientPath = path.join(libDir, "auth-client.ts");
207
-
208
- if (!fs.existsSync(libDir)) {
209
- fs.mkdirSync(libDir, { recursive: true });
210
- }
211
-
212
- const clientContent = `import { createAuthClient } from "better-auth/react";
213
-
214
- export const authClient = createAuthClient({
215
- /** The base URL of the server (optional if you're using the same domain) */
216
- baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000",
217
- });
218
-
219
- // Export commonly used methods for convenience
220
- export const {
221
- signIn,
222
- signUp,
223
- signOut,
224
- getSession,
225
- listSessions,
226
- revokeSession,
227
- revokeOtherSessions,
228
- revokeSessions
229
- } = authClient;
230
-
231
- // IMPORTANT: useSession is a hook available on the authClient instance
232
- //
233
- // ✅ CORRECT - In client components (use the hook from authClient):
234
- // import { authClient } from "@/lib/auth-client";
235
- // const { data: session, isPending } = authClient.useSession();
236
- //
237
- // ✅ CORRECT - In server components or API routes (use async getSession):
238
- // import { authClient } from "@/lib/auth-client";
239
- // const { data: session } = await authClient.getSession();
240
- //
241
- // ❌ WRONG - Don't import useSession from "better-auth/react":
242
- // import { useSession } from "better-auth/react"; // This doesn't exist!
243
- //
244
- // ❌ WRONG - Don't use await in client component function body:
245
- // const { data: session } = await authClient.getSession(); // This will cause an error!
246
- `;
247
-
248
- fs.writeFileSync(clientPath, clientContent, "utf-8");
249
- return path.relative(target, clientPath);
250
- }
251
-
252
- /**
253
- * Generate API route handler
254
- */
255
- export function generateAuthRoute(target: string, scan: ProjectScan): string {
256
- const appDir = detectAppDirectory(target);
257
- const routeDir = path.join(target, appDir, "api", "auth", "[...all]");
258
- const routePath = path.join(routeDir, "route.ts");
259
-
260
- if (!fs.existsSync(routeDir)) {
261
- fs.mkdirSync(routeDir, { recursive: true });
262
- }
263
-
264
- // Calculate relative path to auth file
265
- const libDir = scan.hasSrcDir ? "src/lib" : "lib";
266
- const authImport = scan.hasSrcDir ? "@/lib/auth" : "@/lib/auth";
267
-
268
- const routeContent = `import { auth } from "${authImport}";
269
- import { toNextJsHandler } from "better-auth/next-js";
270
-
271
- export const { POST, GET } = toNextJsHandler(auth);
272
- `;
273
-
274
- fs.writeFileSync(routePath, routeContent, "utf-8");
275
- return path.relative(target, routePath);
276
- }
277
-
278
- /**
279
- * Generate middleware
280
- */
281
- export function generateMiddleware(
282
- target: string,
283
- config: AuthConfig,
284
- scan: ProjectScan
285
- ): string | null {
286
- // Generate middleware if there are protected routes
287
- // Works for both stateless (JWT/JWE in cookies) and database modes
288
- if (config.protectedRoutes.length === 0) {
289
- return null;
290
- }
291
-
292
- const middlewarePath = path.join(target, "middleware.ts");
293
-
294
- // Check if middleware already exists
295
- if (fs.existsSync(middlewarePath)) {
296
- // Try to update existing middleware
297
- let content = fs.readFileSync(middlewarePath, "utf-8");
298
- if (content.includes("better-auth") || content.includes("auth")) {
299
- // Already has auth middleware, skip
300
- return null;
301
- }
302
- }
303
-
304
- const appDir = scan.hasSrcDir ? "src/lib" : "lib";
305
- const authImport = scan.hasSrcDir ? "@/lib/auth" : "@/lib/auth";
306
-
307
- // Build matcher array from protected routes
308
- // Convert protected routes to matcher patterns
309
- const matcherPatterns = config.protectedRoutes.map((route) => {
310
- if (route === "/") {
311
- // Root route - exact match only
312
- return "/";
313
- }
314
-
315
- // Handle wildcard routes (e.g., /dashboard/*)
316
- if (route.endsWith("/*")) {
317
- const baseRoute = route.slice(0, -2); // Remove "/*"
318
- return `${baseRoute}/:path*`;
319
- }
320
-
321
- // For other routes, match the route and all sub-routes
322
- return `${route}/:path*`;
323
- });
324
-
325
- // Add auth pages to matcher so we can redirect authenticated users away
326
- const authPages = ["/auth/login", "/auth/signup"];
327
- const allMatcherPatterns = [...matcherPatterns, ...authPages];
328
-
329
- const middlewareContent = `import { NextRequest, NextResponse } from "next/server";
330
- import { getSessionCookie } from "better-auth/cookies";
331
-
332
- export async function middleware(request: NextRequest) {
333
- const pathname = request.nextUrl.pathname;
334
-
335
- // Check if session cookie exists
336
- const sessionCookie = getSessionCookie(request);
337
-
338
- // Handle auth pages (login/signup)
339
- if (pathname === "/auth/login" || pathname === "/auth/signup") {
340
- // If already authenticated, redirect away from auth pages
341
- if (sessionCookie) {
342
- const redirectParam = request.nextUrl.searchParams.get("redirect");
343
- const redirectTo = redirectParam || "/stackpatch";
344
- return NextResponse.redirect(new URL(redirectTo, request.url));
345
- }
346
- // Not authenticated - allow access to auth pages
347
- return NextResponse.next();
348
- }
349
-
350
- // Handle protected routes (only protected routes reach here thanks to matcher)
351
- if (!sessionCookie) {
352
- // Not authenticated - redirect to login with return URL
353
- const loginUrl = new URL("/auth/login", request.url);
354
- loginUrl.searchParams.set("redirect", pathname);
355
- return NextResponse.redirect(loginUrl);
356
- }
357
-
358
- // Authenticated and accessing protected route - allow access
359
- return NextResponse.next();
360
- }
361
-
362
- export const config = {
363
- matcher: ${JSON.stringify(allMatcherPatterns)}, // Protected routes + auth pages
364
- };
365
- `;
366
-
367
- fs.writeFileSync(middlewarePath, middlewareContent, "utf-8");
368
- return path.relative(target, middlewarePath);
369
- }
370
-
371
- /**
372
- * Generate environment example file
373
- */
374
- export function generateEnvExample(target: string, config: AuthConfig): string {
375
- const envExamplePath = path.join(target, ".env.example");
376
-
377
- // Generate secret - must be at least 32 characters with high entropy
378
- // Using base64 encoding like openssl rand -base64 32
379
- const generateSecret = () => {
380
- let bytes: Uint8Array;
381
-
382
- if (typeof globalThis.crypto !== "undefined" && (globalThis.crypto as any).getRandomValues) {
383
- bytes = (globalThis.crypto as any).getRandomValues(new Uint8Array(32));
384
- } else {
385
- // Fallback for environments without crypto API
386
- bytes = new Uint8Array(32);
387
- for (let i = 0; i < 32; i++) {
388
- bytes[i] = Math.floor(Math.random() * 256);
389
- }
390
- }
391
-
392
- // Convert to base64 (like openssl rand -base64 32)
393
- // This ensures at least 32 characters (base64 of 32 bytes = 44 chars)
394
- if (typeof Buffer !== "undefined") {
395
- // Node.js/Bun environment
396
- return Buffer.from(bytes).toString("base64");
397
- } else {
398
- // Browser-like environment - manual base64 encoding
399
- const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
400
- let result = "";
401
- for (let i = 0; i < bytes.length; i += 3) {
402
- const b1 = bytes[i];
403
- const b2 = bytes[i + 1] || 0;
404
- const b3 = bytes[i + 2] || 0;
405
- const bitmap = (b1 << 16) | (b2 << 8) | b3;
406
- result += base64Chars.charAt((bitmap >> 18) & 63);
407
- result += base64Chars.charAt((bitmap >> 12) & 63);
408
- result += i + 1 < bytes.length ? base64Chars.charAt((bitmap >> 6) & 63) : "=";
409
- result += i + 2 < bytes.length ? base64Chars.charAt(bitmap & 63) : "=";
410
- }
411
- return result;
412
- }
413
- };
414
-
415
- let envContent = `# Better Auth Configuration
416
- BETTER_AUTH_SECRET=${generateSecret()}
417
- BETTER_AUTH_URL=http://localhost:3000
418
-
419
- `;
420
-
421
- if (config.database !== "none" && config.orm === "raw") {
422
- envContent += `# Database
423
- DATABASE_URL=your_database_url_here
424
-
425
- `;
426
- }
427
-
428
- if (config.oauthProviders.includes("google")) {
429
- envContent += `# Google OAuth
430
- GOOGLE_CLIENT_ID=your_google_client_id_here
431
- GOOGLE_CLIENT_SECRET=your_google_client_secret_here
432
-
433
- `;
434
- }
435
-
436
- if (config.oauthProviders.includes("github")) {
437
- envContent += `# GitHub OAuth
438
- GITHUB_CLIENT_ID=your_github_client_id_here
439
- GITHUB_CLIENT_SECRET=your_github_client_secret_here
440
-
441
- `;
442
- }
443
-
444
- fs.writeFileSync(envExamplePath, envContent, "utf-8");
445
- return path.relative(target, envExamplePath);
446
- }
447
-
448
- /**
449
- * Generate stackpatch config file
450
- */
451
- export function generateStackPatchConfig(
452
- target: string,
453
- config: AuthConfig
454
- ): string {
455
- const configPath = path.join(target, "stackpatch.config.json");
456
-
457
- const configContent = {
458
- version: "1.0.0",
459
- patch: "auth",
460
- config: {
461
- sessionMode: config.sessionMode,
462
- database: config.database,
463
- orm: config.orm,
464
- emailPassword: config.emailPassword,
465
- oauthProviders: config.oauthProviders,
466
- addUI: config.addUI,
467
- protectedRoutes: config.protectedRoutes,
468
- },
469
- timestamp: new Date().toISOString(),
470
- };
471
-
472
- fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2), "utf-8");
473
- return path.relative(target, configPath);
474
- }
475
-
476
- /**
477
- * Generate protected routes config file for client-side use
478
- */
479
- export function generateProtectedRoutesConfig(
480
- target: string,
481
- config: AuthConfig,
482
- scan: ProjectScan
483
- ): string {
484
- const libDir = scan.hasSrcDir
485
- ? path.join(target, "src", "lib")
486
- : path.join(target, "lib");
487
- const configPath = path.join(libDir, "protected-routes.ts");
488
-
489
- if (!fs.existsSync(libDir)) {
490
- fs.mkdirSync(libDir, { recursive: true });
491
- }
492
-
493
- const configContent = `/**
494
- * Protected Routes Configuration
495
- *
496
- * This file is auto-generated by StackPatch.
497
- * It defines which routes require authentication.
498
- */
499
-
500
- export const PROTECTED_ROUTES = ${JSON.stringify(config.protectedRoutes, null, 2)} as const;
501
-
502
- export function isProtectedRoute(pathname: string): boolean {
503
- return PROTECTED_ROUTES.some((route) => {
504
- // Handle root route
505
- if (route === "/") {
506
- return pathname === "/";
507
- }
508
-
509
- // Handle wildcard routes (e.g., /dashboard/*)
510
- if (route.endsWith("/*")) {
511
- const baseRoute = route.slice(0, -2); // Remove "/*"
512
- return pathname === baseRoute || pathname.startsWith(baseRoute + "/");
513
- }
514
-
515
- // Handle exact routes and their sub-routes
516
- return pathname === route || pathname.startsWith(route + "/");
517
- });
518
- }
519
- `;
520
-
521
- fs.writeFileSync(configPath, configContent, "utf-8");
522
- return path.relative(target, configPath);
523
- }
524
-
525
- /**
526
- * Generate protected route page (fallback/example)
527
- */
528
- export function generateProtectedPage(target: string, scan: ProjectScan): string | null {
529
- const appDir = detectAppDirectory(target);
530
- const protectedPagePath = path.join(target, appDir, "protected", "page.tsx");
531
-
532
- // Create protected directory if it doesn't exist
533
- const protectedDir = path.join(target, appDir, "protected");
534
- if (!fs.existsSync(protectedDir)) {
535
- fs.mkdirSync(protectedDir, { recursive: true });
536
- }
537
-
538
- // Use client-side auth check with ProtectedRoute component
539
- const componentsDir = scan.hasSrcDir ? "src/components" : "components";
540
- const protectedRouteImport = scan.hasSrcDir ? "@/components/protected-route" : "@/components/protected-route";
541
- const clientImport = scan.hasSrcDir ? "@/lib/auth-client" : "@/lib/auth-client";
542
-
543
- const pageContent = `"use client";
544
-
545
- import { ProtectedRoute } from "${protectedRouteImport}";
546
- import { useSession } from "${clientImport}";
547
-
548
- export default function ProtectedPage() {
549
- const { data: session } = useSession();
550
-
551
- return (
552
- <ProtectedRoute>
553
- <div className="container mx-auto px-4 py-8">
554
- <h1 className="text-3xl font-bold">Protected Page</h1>
555
- <p className="mt-4 text-zinc-600 dark:text-zinc-400">
556
- This page is protected. Only authenticated users can see this.
557
- </p>
558
- <div className="mt-4">
559
- <p>Email: {session?.user?.email}</p>
560
- </div>
561
- </div>
562
- </ProtectedRoute>
563
- );
564
- }
565
- `;
566
-
567
- fs.writeFileSync(protectedPagePath, pageContent, "utf-8");
568
- return path.relative(target, protectedPagePath);
569
- }