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
package/src/auth/index.ts
DELETED
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import fse from "fs-extra";
|
|
5
|
-
import { detectAppDirectory, detectComponentsDirectory } from "../utils/paths.js";
|
|
6
|
-
import { ProgressTracker, withSpinner } from "../ui/progress.js";
|
|
7
|
-
import { installDependencies } from "../utils/dependencies.js";
|
|
8
|
-
import {
|
|
9
|
-
performProjectScan,
|
|
10
|
-
collectAuthConfig,
|
|
11
|
-
type AuthConfig,
|
|
12
|
-
} from "./setup.js";
|
|
13
|
-
import type { ProjectScan } from "../utils/scanner.js";
|
|
14
|
-
import {
|
|
15
|
-
generateAuthInstance,
|
|
16
|
-
generateAuthClient,
|
|
17
|
-
generateAuthRoute,
|
|
18
|
-
generateMiddleware,
|
|
19
|
-
generateEnvExample,
|
|
20
|
-
generateStackPatchConfig,
|
|
21
|
-
generateProtectedRoutesConfig,
|
|
22
|
-
generateProtectedPage,
|
|
23
|
-
} from "./generator.js";
|
|
24
|
-
import { BOILERPLATE_ROOT } from "../config.js";
|
|
25
|
-
import { copyFiles } from "../fileOps/copy.js";
|
|
26
|
-
import { writeManifest } from "../manifest.js";
|
|
27
|
-
import { MANIFEST_VERSION, type StackPatchManifest } from "../config.js";
|
|
28
|
-
import { updateLayoutForToaster, updateLayoutForAuthWrapper } from "../fileOps/layout.js";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Main authentication setup function using new CLI flow
|
|
32
|
-
*/
|
|
33
|
-
export async function setupAuthNew(target: string): Promise<boolean> {
|
|
34
|
-
const tracker = new ProgressTracker();
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
// Step 1: Scan project
|
|
38
|
-
const scan = performProjectScan(target);
|
|
39
|
-
|
|
40
|
-
// Collect configuration
|
|
41
|
-
const config = await collectAuthConfig(target, scan);
|
|
42
|
-
if (!config) {
|
|
43
|
-
console.log(chalk.yellow("\n← Setup cancelled"));
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
tracker.addStep("Installing dependencies");
|
|
48
|
-
tracker.addStep("Creating auth configuration");
|
|
49
|
-
tracker.addStep("Setting up API routes");
|
|
50
|
-
tracker.addStep("Configuring middleware");
|
|
51
|
-
tracker.addStep("Adding UI components");
|
|
52
|
-
tracker.addStep("Protecting routes");
|
|
53
|
-
|
|
54
|
-
tracker.startStep(0);
|
|
55
|
-
|
|
56
|
-
// Install dependencies
|
|
57
|
-
// Note: better-auth includes React hooks, no need for separate better-auth/react package
|
|
58
|
-
const deps = ["better-auth"];
|
|
59
|
-
// Only add better-sqlite3 if database mode is selected AND sqlite + raw driver
|
|
60
|
-
if (config.sessionMode === "database" && config.database === "sqlite" && config.orm === "raw") {
|
|
61
|
-
deps.push("better-sqlite3");
|
|
62
|
-
}
|
|
63
|
-
// Add react-hot-toast if UI is enabled
|
|
64
|
-
if (config.addUI) {
|
|
65
|
-
deps.push("react-hot-toast");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
installDependencies(target, deps);
|
|
70
|
-
tracker.completeStep(0);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
tracker.failStep(0);
|
|
73
|
-
console.log(chalk.yellow("\n⚠️ Dependency installation failed. You can install them manually:"));
|
|
74
|
-
console.log(chalk.white(` cd ${path.relative(process.cwd(), target)}`));
|
|
75
|
-
console.log(chalk.white(` pnpm add ${deps.join(" ")}`));
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Generate files
|
|
80
|
-
tracker.startStep(1);
|
|
81
|
-
const authPath = generateAuthInstance(target, config, scan);
|
|
82
|
-
const clientPath = generateAuthClient(target, scan);
|
|
83
|
-
generateEnvExample(target, config);
|
|
84
|
-
generateStackPatchConfig(target, config);
|
|
85
|
-
const protectedRoutesConfigPath = generateProtectedRoutesConfig(target, config, scan);
|
|
86
|
-
tracker.completeStep(1);
|
|
87
|
-
|
|
88
|
-
tracker.startStep(2);
|
|
89
|
-
const routePath = generateAuthRoute(target, scan);
|
|
90
|
-
tracker.completeStep(2);
|
|
91
|
-
|
|
92
|
-
tracker.startStep(3);
|
|
93
|
-
const middlewarePath = generateMiddleware(target, config, scan);
|
|
94
|
-
if (middlewarePath) {
|
|
95
|
-
tracker.completeStep(3);
|
|
96
|
-
} else {
|
|
97
|
-
tracker.completeStep(3);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Copy UI files if requested
|
|
101
|
-
const addedFiles: string[] = [authPath, clientPath, routePath, protectedRoutesConfigPath];
|
|
102
|
-
if (middlewarePath) {
|
|
103
|
-
addedFiles.push(middlewarePath);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Track modified files (layout.tsx)
|
|
107
|
-
const modifiedFiles: Array<{ path: string; originalContent: string }> = [];
|
|
108
|
-
|
|
109
|
-
if (config.addUI) {
|
|
110
|
-
tracker.startStep(4);
|
|
111
|
-
const uiSrc = path.join(BOILERPLATE_ROOT, "auth");
|
|
112
|
-
|
|
113
|
-
// SAFETY: Always preserve the root page.tsx - never overwrite it
|
|
114
|
-
// We only want to add /stackpatch page, not modify the existing root page
|
|
115
|
-
const appDir = detectAppDirectory(target);
|
|
116
|
-
const rootPagePath = path.join(target, appDir, "page.tsx");
|
|
117
|
-
const rootPageExists = fs.existsSync(rootPagePath);
|
|
118
|
-
|
|
119
|
-
// Backup root page if it exists (before copying, we might temporarily overwrite it)
|
|
120
|
-
let rootPageOriginalContent: string | undefined;
|
|
121
|
-
if (rootPageExists) {
|
|
122
|
-
rootPageOriginalContent = fs.readFileSync(rootPagePath, "utf-8");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const copyResult = await copyFiles(uiSrc, target);
|
|
126
|
-
if (copyResult.success) {
|
|
127
|
-
addedFiles.push(...copyResult.addedFiles);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ALWAYS restore the original root page.tsx - we never want to change it
|
|
131
|
-
// The boilerplate page.tsx is only for reference, we preserve user's existing code
|
|
132
|
-
const copiedRootPagePath = path.join(target, appDir, "page.tsx");
|
|
133
|
-
if (rootPageOriginalContent) {
|
|
134
|
-
// Restore the original content immediately - preserve all existing code, pages, links
|
|
135
|
-
fs.writeFileSync(copiedRootPagePath, rootPageOriginalContent, "utf-8");
|
|
136
|
-
// Remove from addedFiles since we're restoring the original (not using boilerplate version)
|
|
137
|
-
const rootPageRelative = path.relative(target, copiedRootPagePath).replace(/\\/g, "/");
|
|
138
|
-
const index = addedFiles.indexOf(rootPageRelative);
|
|
139
|
-
if (index > -1) {
|
|
140
|
-
addedFiles.splice(index, 1);
|
|
141
|
-
}
|
|
142
|
-
} else if (fs.existsSync(copiedRootPagePath)) {
|
|
143
|
-
// Original didn't exist, but boilerplate copied one - delete it since user didn't have one
|
|
144
|
-
// We don't want to add a redirecting page.tsx if user didn't have one
|
|
145
|
-
fs.unlinkSync(copiedRootPagePath);
|
|
146
|
-
const rootPageRelative = path.relative(target, copiedRootPagePath).replace(/\\/g, "/");
|
|
147
|
-
const index = addedFiles.indexOf(rootPageRelative);
|
|
148
|
-
if (index > -1) {
|
|
149
|
-
addedFiles.splice(index, 1);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Update layout.tsx to include AuthWrapper and Toaster component
|
|
154
|
-
// We track the original content so we can restore the file to its state before StackPatch changes
|
|
155
|
-
|
|
156
|
-
// Add AuthWrapper first (wraps children)
|
|
157
|
-
const authWrapperResult = updateLayoutForAuthWrapper(target);
|
|
158
|
-
if (authWrapperResult.modified && authWrapperResult.originalContent) {
|
|
159
|
-
// Track the original content before AuthWrapper was added
|
|
160
|
-
modifiedFiles.push({
|
|
161
|
-
path: authWrapperResult.filePath,
|
|
162
|
-
originalContent: authWrapperResult.originalContent,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Add Toaster (uses the content after AuthWrapper was added, but we track the original)
|
|
167
|
-
const toasterLayoutResult = updateLayoutForToaster(target);
|
|
168
|
-
// Only track if AuthWrapper wasn't already tracked (use the original originalContent)
|
|
169
|
-
if (toasterLayoutResult.modified) {
|
|
170
|
-
const existingIndex = modifiedFiles.findIndex((f) => f.path === toasterLayoutResult.filePath);
|
|
171
|
-
if (existingIndex === -1) {
|
|
172
|
-
// AuthWrapper wasn't added, track Toaster's original
|
|
173
|
-
if (toasterLayoutResult.originalContent) {
|
|
174
|
-
modifiedFiles.push({
|
|
175
|
-
path: toasterLayoutResult.filePath,
|
|
176
|
-
originalContent: toasterLayoutResult.originalContent,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
// If AuthWrapper was already tracked, keep the original originalContent (before any modifications)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
tracker.completeStep(4);
|
|
184
|
-
} else {
|
|
185
|
-
tracker.completeStep(4);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
tracker.startStep(5);
|
|
189
|
-
// Generate protected page for /dashboard or custom routes (not for "/")
|
|
190
|
-
const needsProtectedPage = config.protectedRoutes.some(
|
|
191
|
-
(route) => route !== "/" && (route === "/dashboard" || !route.startsWith("/"))
|
|
192
|
-
);
|
|
193
|
-
if (needsProtectedPage || config.protectedRoutes.includes("/dashboard")) {
|
|
194
|
-
const protectedPagePath = generateProtectedPage(target, scan);
|
|
195
|
-
if (protectedPagePath) {
|
|
196
|
-
addedFiles.push(protectedPagePath);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
tracker.completeStep(5);
|
|
200
|
-
|
|
201
|
-
// Create manifest
|
|
202
|
-
const manifest: StackPatchManifest = {
|
|
203
|
-
version: MANIFEST_VERSION,
|
|
204
|
-
patchName: "auth",
|
|
205
|
-
target,
|
|
206
|
-
timestamp: new Date().toISOString(),
|
|
207
|
-
files: {
|
|
208
|
-
added: addedFiles,
|
|
209
|
-
modified: modifiedFiles,
|
|
210
|
-
backedUp: [],
|
|
211
|
-
envFiles: [".env.example"],
|
|
212
|
-
},
|
|
213
|
-
dependencies: deps,
|
|
214
|
-
oauthProviders: config.oauthProviders,
|
|
215
|
-
};
|
|
216
|
-
writeManifest(target, manifest);
|
|
217
|
-
|
|
218
|
-
// Show success message
|
|
219
|
-
showSuccessMessage(target, config, scan);
|
|
220
|
-
|
|
221
|
-
return true;
|
|
222
|
-
} catch (error) {
|
|
223
|
-
tracker.failStep(0);
|
|
224
|
-
console.error(chalk.red("Error setting up Better Auth:"), error);
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Show success message with next steps
|
|
231
|
-
*/
|
|
232
|
-
function showSuccessMessage(
|
|
233
|
-
target: string,
|
|
234
|
-
config: AuthConfig,
|
|
235
|
-
scan: ProjectScan
|
|
236
|
-
): void {
|
|
237
|
-
console.log(chalk.green.bold("\n✅ Auth installed successfully\n"));
|
|
238
|
-
|
|
239
|
-
console.log(chalk.blue.bold("📦 Configuration Summary:"));
|
|
240
|
-
console.log(chalk.white(` Session Mode: ${config.sessionMode === "stateless" ? "Stateless (JWT)" : "Database"}`));
|
|
241
|
-
if (config.sessionMode === "database" && config.database !== "none") {
|
|
242
|
-
console.log(chalk.white(` Database: ${config.database}`));
|
|
243
|
-
console.log(chalk.white(` ORM: ${config.orm}`));
|
|
244
|
-
}
|
|
245
|
-
console.log(chalk.white(` Email/Password: ${config.emailPassword ? "Enabled" : "Disabled"}`));
|
|
246
|
-
if (config.oauthProviders.length > 0) {
|
|
247
|
-
console.log(chalk.white(` OAuth Providers: ${config.oauthProviders.join(", ")}`));
|
|
248
|
-
} else {
|
|
249
|
-
console.log(chalk.white(` OAuth Providers: None`));
|
|
250
|
-
}
|
|
251
|
-
console.log(chalk.white(` UI Components: ${config.addUI ? "Added" : "Not added"}`));
|
|
252
|
-
console.log(chalk.white(` Protected Routes: ${config.protectedRoutes.join(", ")}`));
|
|
253
|
-
if (config.protectedRoutes.some((r) => r.endsWith("/*"))) {
|
|
254
|
-
console.log(chalk.blue(" 💡 Tip: Routes ending with /* protect all sub-routes"));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
console.log(chalk.blue.bold("\n📁 Created Files:"));
|
|
258
|
-
const libDir = scan.hasSrcDir ? "src/lib" : "lib";
|
|
259
|
-
console.log(chalk.white(` - ${libDir}/auth.ts`));
|
|
260
|
-
console.log(chalk.white(` - ${libDir}/auth-client.ts`));
|
|
261
|
-
console.log(chalk.white(` - app/api/auth/[...all]/route.ts`));
|
|
262
|
-
if (config.protectedRoutes.length > 0) {
|
|
263
|
-
console.log(chalk.white(` - middleware.ts`));
|
|
264
|
-
}
|
|
265
|
-
if (config.protectedRoutes.length > 0) {
|
|
266
|
-
console.log(chalk.white(` - ${libDir}/protected-routes.ts`));
|
|
267
|
-
}
|
|
268
|
-
if (config.addUI) {
|
|
269
|
-
console.log(chalk.white(` - app/auth/login/page.tsx`));
|
|
270
|
-
console.log(chalk.white(` - app/auth/signup/page.tsx`));
|
|
271
|
-
console.log(chalk.white(` - app/stackpatch/page.tsx`));
|
|
272
|
-
}
|
|
273
|
-
console.log(chalk.white(` - .env.example`));
|
|
274
|
-
|
|
275
|
-
console.log(chalk.blue.bold("\n📦 Installed Dependencies:"));
|
|
276
|
-
console.log(chalk.white(" - better-auth"));
|
|
277
|
-
if (config.addUI) {
|
|
278
|
-
console.log(chalk.white(" - react-hot-toast"));
|
|
279
|
-
}
|
|
280
|
-
if (config.sessionMode === "database" && config.database === "sqlite" && config.orm === "raw") {
|
|
281
|
-
console.log(chalk.white(" - better-sqlite3"));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
console.log(chalk.blue.bold("\n🚀 Next Steps:\n"));
|
|
285
|
-
console.log(chalk.white("1. Create .env.local file:"));
|
|
286
|
-
console.log(chalk.cyan(` cp .env.example .env.local`));
|
|
287
|
-
console.log(chalk.white("\n2. Add OAuth credentials to .env.local:"));
|
|
288
|
-
if (config.oauthProviders.includes("google")) {
|
|
289
|
-
console.log(chalk.yellow(" - Get Google OAuth credentials:"));
|
|
290
|
-
console.log(chalk.white(" https://console.cloud.google.com/apis/credentials"));
|
|
291
|
-
console.log(chalk.white(" Add redirect URI: http://localhost:3000/api/auth/callback/google"));
|
|
292
|
-
console.log(chalk.cyan(" GOOGLE_CLIENT_ID=your_client_id"));
|
|
293
|
-
console.log(chalk.cyan(" GOOGLE_CLIENT_SECRET=your_client_secret"));
|
|
294
|
-
}
|
|
295
|
-
if (config.oauthProviders.includes("github")) {
|
|
296
|
-
console.log(chalk.yellow(" - Get GitHub OAuth credentials:"));
|
|
297
|
-
console.log(chalk.white(" https://github.com/settings/developers"));
|
|
298
|
-
console.log(chalk.white(" Add callback URL: http://localhost:3000/api/auth/callback/github"));
|
|
299
|
-
console.log(chalk.cyan(" GITHUB_CLIENT_ID=your_client_id"));
|
|
300
|
-
console.log(chalk.cyan(" GITHUB_CLIENT_SECRET=your_client_secret"));
|
|
301
|
-
}
|
|
302
|
-
if (config.sessionMode === "database" && config.database !== "none") {
|
|
303
|
-
console.log(chalk.white("\n3. Generate database schema:"));
|
|
304
|
-
console.log(chalk.cyan(" npx @better-auth/cli generate"));
|
|
305
|
-
console.log(chalk.white(" Or migrate directly:"));
|
|
306
|
-
console.log(chalk.cyan(" npx @better-auth/cli migrate"));
|
|
307
|
-
console.log(chalk.white("\n4. Start dev server:"));
|
|
308
|
-
} else {
|
|
309
|
-
console.log(chalk.white("\n3. Start dev server:"));
|
|
310
|
-
}
|
|
311
|
-
console.log(chalk.cyan(" pnpm dev"));
|
|
312
|
-
console.log(chalk.white("\n5. Visit your app:"));
|
|
313
|
-
console.log(chalk.cyan(" http://localhost:3000/stackpatch"));
|
|
314
|
-
console.log(chalk.gray("\n 💡 Tip: To change the /stackpatch route, see comments in:"));
|
|
315
|
-
console.log(chalk.gray(` - ${scan.hasSrcDir ? "src" : ""}app/stackpatch/page.tsx`));
|
|
316
|
-
console.log(chalk.gray(` - ${scan.hasSrcDir ? "src" : ""}app/page.tsx`));
|
|
317
|
-
console.log(chalk.gray(` - ${scan.hasSrcDir ? "src" : ""}app/auth/login/page.tsx`));
|
|
318
|
-
|
|
319
|
-
console.log(chalk.blue.bold("\n🔒 Protected Routes Guide:"));
|
|
320
|
-
console.log(chalk.white(" Routes are automatically protected based on your configuration."));
|
|
321
|
-
console.log(chalk.white(" Examples:"));
|
|
322
|
-
console.log(chalk.cyan(" - /dashboard → Protects only /dashboard"));
|
|
323
|
-
console.log(chalk.cyan(" - /dashboard/* → Protects /dashboard and ALL sub-routes"));
|
|
324
|
-
console.log(chalk.cyan(" - /admin/* → Protects /admin and ALL sub-routes"));
|
|
325
|
-
console.log(chalk.white(" To modify protected routes, edit:"));
|
|
326
|
-
console.log(chalk.cyan(` - ${scan.hasSrcDir ? "src" : ""}lib/protected-routes.ts`));
|
|
327
|
-
|
|
328
|
-
console.log(chalk.blue.bold("\n📚 Documentation:"));
|
|
329
|
-
console.log(chalk.white(" https://better-auth.dev"));
|
|
330
|
-
console.log(chalk.white(" https://stackpatch.dev/docs/auth\n"));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Legacy function for backward compatibility
|
|
335
|
-
* @deprecated Use setupAuthNew instead
|
|
336
|
-
*/
|
|
337
|
-
export async function setupAuth(
|
|
338
|
-
target: string,
|
|
339
|
-
selectedProviders: string[]
|
|
340
|
-
): Promise<boolean> {
|
|
341
|
-
// Redirect to new flow
|
|
342
|
-
return setupAuthNew(target);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Legacy OAuth provider selection
|
|
347
|
-
* @deprecated
|
|
348
|
-
*/
|
|
349
|
-
export async function askOAuthProviders(): Promise<string[]> {
|
|
350
|
-
// This is kept for backward compatibility but won't be used in new flow
|
|
351
|
-
return [];
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Legacy database type selection
|
|
356
|
-
* @deprecated
|
|
357
|
-
*/
|
|
358
|
-
export async function askDatabaseType(): Promise<string> {
|
|
359
|
-
return "sqlite";
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Legacy OAuth setup instructions
|
|
364
|
-
* @deprecated
|
|
365
|
-
*/
|
|
366
|
-
export async function showOAuthSetupInstructions(
|
|
367
|
-
target: string,
|
|
368
|
-
selectedProviders: string[] = [],
|
|
369
|
-
dbType: string = "sqlite"
|
|
370
|
-
): Promise<void> {
|
|
371
|
-
// Legacy function, kept for compatibility
|
|
372
|
-
}
|
package/src/auth/setup.ts
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import inquirer from "inquirer";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import { detectAppDirectory, detectComponentsDirectory } from "../utils/paths.js";
|
|
6
|
-
import { scanProject, formatScanResults, type ProjectScan } from "../utils/scanner.js";
|
|
7
|
-
import { ProgressTracker } from "../ui/progress.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Auth configuration options
|
|
11
|
-
*/
|
|
12
|
-
export interface AuthConfig {
|
|
13
|
-
sessionMode: "database" | "stateless";
|
|
14
|
-
database: "postgres" | "mysql" | "sqlite" | "mongodb" | "none";
|
|
15
|
-
orm: "drizzle" | "prisma" | "raw" | "none";
|
|
16
|
-
emailPassword: boolean;
|
|
17
|
-
oauthProviders: string[];
|
|
18
|
-
addUI: boolean;
|
|
19
|
-
protectedRoutes: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Step 1: Project Scan (automatic)
|
|
24
|
-
*/
|
|
25
|
-
export function performProjectScan(target: string): ProjectScan {
|
|
26
|
-
return scanProject(target);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Step 2: Session Mode Selection
|
|
31
|
-
*/
|
|
32
|
-
export async function askSessionMode(): Promise<"database" | "stateless"> {
|
|
33
|
-
const { mode } = await inquirer.prompt([
|
|
34
|
-
{
|
|
35
|
-
type: "rawlist",
|
|
36
|
-
name: "mode",
|
|
37
|
-
message: "Choose session mode:",
|
|
38
|
-
choices: [
|
|
39
|
-
{ name: "Database (persistent sessions)", value: "database" },
|
|
40
|
-
{ name: "Stateless (JWT only, no database)", value: "stateless" },
|
|
41
|
-
],
|
|
42
|
-
default: "database",
|
|
43
|
-
},
|
|
44
|
-
]);
|
|
45
|
-
return mode;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Step 3: Database Selection (only shown if database mode is selected)
|
|
50
|
-
*/
|
|
51
|
-
export async function askDatabase(): Promise<"postgres" | "mysql" | "sqlite" | "mongodb"> {
|
|
52
|
-
const { database } = await inquirer.prompt([
|
|
53
|
-
{
|
|
54
|
-
type: "rawlist",
|
|
55
|
-
name: "database",
|
|
56
|
-
message: "Choose database:",
|
|
57
|
-
choices: [
|
|
58
|
-
{ name: "PostgreSQL", value: "postgres" },
|
|
59
|
-
{ name: "MySQL", value: "mysql" },
|
|
60
|
-
{ name: "SQLite", value: "sqlite" },
|
|
61
|
-
{ name: "MongoDB", value: "mongodb" },
|
|
62
|
-
],
|
|
63
|
-
default: "postgres",
|
|
64
|
-
},
|
|
65
|
-
]);
|
|
66
|
-
return database;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Step 4: ORM Selection (only shown if database mode is selected)
|
|
71
|
-
*/
|
|
72
|
-
export async function askORM(): Promise<"drizzle" | "prisma" | "raw"> {
|
|
73
|
-
const { orm } = await inquirer.prompt([
|
|
74
|
-
{
|
|
75
|
-
type: "rawlist",
|
|
76
|
-
name: "orm",
|
|
77
|
-
message: "Choose ORM:",
|
|
78
|
-
choices: [
|
|
79
|
-
{ name: "Drizzle", value: "drizzle" },
|
|
80
|
-
{ name: "Prisma", value: "prisma" },
|
|
81
|
-
{ name: "Raw driver", value: "raw" },
|
|
82
|
-
],
|
|
83
|
-
default: "drizzle",
|
|
84
|
-
},
|
|
85
|
-
]);
|
|
86
|
-
return orm;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Step 5: Auth Providers
|
|
91
|
-
*/
|
|
92
|
-
export async function askAuthProviders(): Promise<{ emailPassword: boolean; oauth: string[] }> {
|
|
93
|
-
const { emailPassword } = await inquirer.prompt([
|
|
94
|
-
{
|
|
95
|
-
type: "rawlist",
|
|
96
|
-
name: "emailPassword",
|
|
97
|
-
message: "Enable Email + Password?",
|
|
98
|
-
choices: [
|
|
99
|
-
{ name: "Yes", value: "yes" },
|
|
100
|
-
{ name: "No", value: "no" },
|
|
101
|
-
],
|
|
102
|
-
default: "yes",
|
|
103
|
-
},
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
const emailPasswordBool = emailPassword === "yes";
|
|
107
|
-
|
|
108
|
-
const { oauth } = await inquirer.prompt([
|
|
109
|
-
{
|
|
110
|
-
type: "checkbox",
|
|
111
|
-
name: "oauth",
|
|
112
|
-
message: "Add OAuth providers?",
|
|
113
|
-
choices: [
|
|
114
|
-
{ name: "GitHub", value: "github" },
|
|
115
|
-
{ name: "Google", value: "google" },
|
|
116
|
-
{ name: "None", value: "none" },
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
// Remove "none" if other providers are selected
|
|
122
|
-
const oauthProviders = oauth.filter((p: string) => p !== "none" || oauth.length === 1);
|
|
123
|
-
|
|
124
|
-
return { emailPassword: emailPasswordBool, oauth: oauthProviders };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Step 6: UI Selection
|
|
129
|
-
*/
|
|
130
|
-
export async function askUI(): Promise<boolean> {
|
|
131
|
-
const { addUI } = await inquirer.prompt([
|
|
132
|
-
{
|
|
133
|
-
type: "rawlist",
|
|
134
|
-
name: "addUI",
|
|
135
|
-
message: "Add prebuilt auth UI?",
|
|
136
|
-
choices: [
|
|
137
|
-
{ name: "Yes (recommended)", value: "yes" },
|
|
138
|
-
{ name: "No", value: "no" },
|
|
139
|
-
],
|
|
140
|
-
default: "yes",
|
|
141
|
-
},
|
|
142
|
-
]);
|
|
143
|
-
return addUI === "yes";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Step 7: Protected Routes
|
|
148
|
-
*/
|
|
149
|
-
export async function askProtectedRoutes(): Promise<string[]> {
|
|
150
|
-
const { routeType } = await inquirer.prompt([
|
|
151
|
-
{
|
|
152
|
-
type: "rawlist",
|
|
153
|
-
name: "routeType",
|
|
154
|
-
message: "Which route should be protected?",
|
|
155
|
-
choices: [
|
|
156
|
-
{ name: "/ and /stackpatch (default)", value: "default" },
|
|
157
|
-
{ name: "/dashboard", value: "dashboard" },
|
|
158
|
-
{ name: "/dashboard/* (protects dashboard and all sub-routes)", value: "dashboard-wildcard" },
|
|
159
|
-
{ name: "Custom", value: "custom" },
|
|
160
|
-
],
|
|
161
|
-
default: "default",
|
|
162
|
-
},
|
|
163
|
-
]);
|
|
164
|
-
|
|
165
|
-
if (routeType === "custom") {
|
|
166
|
-
const { customRoutes } = await inquirer.prompt([
|
|
167
|
-
{
|
|
168
|
-
type: "input",
|
|
169
|
-
name: "customRoutes",
|
|
170
|
-
message: "Enter routes to protect (comma-separated, e.g., /dashboard,/profile):",
|
|
171
|
-
validate: (input: string) => {
|
|
172
|
-
if (!input.trim()) {
|
|
173
|
-
return "Please enter at least one route";
|
|
174
|
-
}
|
|
175
|
-
return true;
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
const routes = customRoutes.split(",").map((r: string) => r.trim());
|
|
181
|
-
|
|
182
|
-
// Show helpful message about wildcard support
|
|
183
|
-
console.log(chalk.blue("\n💡 Tip: Use /* to protect all sub-routes"));
|
|
184
|
-
console.log(chalk.gray(" Examples:"));
|
|
185
|
-
console.log(chalk.gray(" - /dashboard/* protects /dashboard and all sub-routes"));
|
|
186
|
-
console.log(chalk.gray(" - /admin/* protects /admin and all sub-routes"));
|
|
187
|
-
console.log(chalk.gray(" - /dashboard protects only /dashboard (not sub-routes)"));
|
|
188
|
-
|
|
189
|
-
return routes;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (routeType === "default") {
|
|
193
|
-
return ["/", "/stackpatch"];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (routeType === "dashboard-wildcard") {
|
|
197
|
-
return ["/dashboard/*"];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return ["/dashboard"];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Step 8: Confirmation
|
|
205
|
-
*/
|
|
206
|
-
export async function showConfirmation(config: AuthConfig): Promise<boolean> {
|
|
207
|
-
console.log(chalk.blue.bold("\nReady to apply patch:\n"));
|
|
208
|
-
|
|
209
|
-
const items: string[] = [];
|
|
210
|
-
items.push("• Install better-auth");
|
|
211
|
-
items.push("• Create auth config");
|
|
212
|
-
items.push("• Add API route");
|
|
213
|
-
if (config.sessionMode === "database") {
|
|
214
|
-
items.push("• Add middleware");
|
|
215
|
-
}
|
|
216
|
-
if (config.addUI) {
|
|
217
|
-
items.push("• Add login/signup UI");
|
|
218
|
-
}
|
|
219
|
-
config.protectedRoutes.forEach((route) => {
|
|
220
|
-
items.push(`• Protect ${route}`);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
items.forEach((item) => console.log(chalk.white(item)));
|
|
224
|
-
|
|
225
|
-
const { confirm } = await inquirer.prompt([
|
|
226
|
-
{
|
|
227
|
-
type: "list",
|
|
228
|
-
name: "confirm",
|
|
229
|
-
message: "\nContinue?",
|
|
230
|
-
choices: [
|
|
231
|
-
{ name: "Yes", value: "yes" },
|
|
232
|
-
{ name: "Cancel", value: "cancel" },
|
|
233
|
-
],
|
|
234
|
-
default: "yes",
|
|
235
|
-
},
|
|
236
|
-
]);
|
|
237
|
-
|
|
238
|
-
return confirm === "yes";
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Collect all configuration through interactive prompts
|
|
243
|
-
*/
|
|
244
|
-
export async function collectAuthConfig(
|
|
245
|
-
target: string,
|
|
246
|
-
scan: ProjectScan
|
|
247
|
-
): Promise<AuthConfig | null> {
|
|
248
|
-
// Step 1: Show scan results
|
|
249
|
-
console.log(chalk.blue.bold("\nStep 1 — Project Scan\n"));
|
|
250
|
-
const scanResults = formatScanResults(scan);
|
|
251
|
-
scanResults.forEach((result) => console.log(chalk.green(result)));
|
|
252
|
-
console.log();
|
|
253
|
-
|
|
254
|
-
// Step 2: Session Mode
|
|
255
|
-
const sessionMode = await askSessionMode();
|
|
256
|
-
|
|
257
|
-
// Step 3-4: Database and ORM (only if database mode)
|
|
258
|
-
let database: "postgres" | "mysql" | "sqlite" | "mongodb" | "none" = "none";
|
|
259
|
-
let orm: "drizzle" | "prisma" | "raw" | "none" = "none";
|
|
260
|
-
|
|
261
|
-
if (sessionMode === "database") {
|
|
262
|
-
database = await askDatabase();
|
|
263
|
-
orm = await askORM();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Step 5: Auth Providers
|
|
267
|
-
// Both email/password and OAuth work in stateless mode
|
|
268
|
-
const { emailPassword, oauth } = await askAuthProviders();
|
|
269
|
-
|
|
270
|
-
// Step 6: UI
|
|
271
|
-
const addUI = await askUI();
|
|
272
|
-
|
|
273
|
-
// Step 7: Protected Routes
|
|
274
|
-
const protectedRoutes = await askProtectedRoutes();
|
|
275
|
-
|
|
276
|
-
const config: AuthConfig = {
|
|
277
|
-
sessionMode,
|
|
278
|
-
database,
|
|
279
|
-
orm,
|
|
280
|
-
emailPassword,
|
|
281
|
-
oauthProviders: oauth,
|
|
282
|
-
addUI,
|
|
283
|
-
protectedRoutes,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
// Step 8: Confirmation
|
|
287
|
-
const confirmed = await showConfirmation(config);
|
|
288
|
-
if (!confirmed) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return config;
|
|
293
|
-
}
|