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.
- package/README.md +76 -69
- package/bin/stackpatch.js +79 -0
- package/bin/stackpatch.ts +2445 -3
- package/boilerplate/auth/app/api/auth/[...nextauth]/route.ts +124 -0
- package/boilerplate/auth/app/api/auth/signup/route.ts +45 -0
- package/boilerplate/auth/app/auth/login/page.tsx +24 -50
- package/boilerplate/auth/app/auth/signup/page.tsx +56 -69
- package/boilerplate/auth/app/dashboard/page.tsx +82 -0
- package/boilerplate/auth/app/login/page.tsx +136 -0
- package/boilerplate/auth/app/page.tsx +48 -0
- package/boilerplate/auth/components/auth-button.tsx +43 -0
- package/boilerplate/auth/components/auth-navbar.tsx +118 -0
- package/boilerplate/auth/components/protected-route.tsx +74 -0
- package/boilerplate/auth/components/session-provider.tsx +11 -0
- package/boilerplate/auth/middleware.ts +51 -0
- package/package.json +5 -6
- package/boilerplate/auth/app/stackpatch/page.tsx +0 -269
- package/boilerplate/auth/components/auth-wrapper.tsx +0 -61
- package/src/auth/generator.ts +0 -569
- package/src/auth/index.ts +0 -372
- package/src/auth/setup.ts +0 -293
- package/src/commands/add.ts +0 -112
- package/src/commands/create.ts +0 -128
- package/src/commands/revert.ts +0 -389
- package/src/config.ts +0 -52
- package/src/fileOps/copy.ts +0 -224
- package/src/fileOps/layout.ts +0 -304
- package/src/fileOps/protected.ts +0 -67
- package/src/index.ts +0 -215
- package/src/manifest.ts +0 -87
- package/src/ui/logo.ts +0 -24
- package/src/ui/progress.ts +0 -82
- package/src/utils/dependencies.ts +0 -114
- package/src/utils/deps-check.ts +0 -45
- package/src/utils/files.ts +0 -58
- package/src/utils/paths.ts +0 -217
- 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
|
-
}
|
package/src/auth/generator.ts
DELETED
|
@@ -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
|
-
}
|