@smittdev/next-jwt-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/files/lib/auth/client/index.ts +2 -0
- package/dist/files/lib/auth/client/provider.tsx +424 -0
- package/dist/files/lib/auth/config.ts +54 -0
- package/dist/files/lib/auth/core/config.ts +57 -0
- package/dist/files/lib/auth/core/cookies.ts +92 -0
- package/dist/files/lib/auth/core/index.ts +14 -0
- package/dist/files/lib/auth/core/jwt.ts +65 -0
- package/dist/files/lib/auth/index.ts +112 -0
- package/dist/files/lib/auth/middleware/auth-middleware.ts +191 -0
- package/dist/files/lib/auth/middleware/index.ts +3 -0
- package/dist/files/lib/auth/server/actions.ts +352 -0
- package/dist/files/lib/auth/server/fetchers.ts +40 -0
- package/dist/files/lib/auth/server/index.ts +12 -0
- package/dist/files/lib/auth/server/session.ts +158 -0
- package/dist/files/lib/auth/types.ts +227 -0
- package/dist/index.js +1128 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/commands/init.ts
|
|
27
|
+
var import_prompts = __toESM(require("prompts"));
|
|
28
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
29
|
+
|
|
30
|
+
// src/utils/fs.ts
|
|
31
|
+
var import_fs = __toESM(require("fs"));
|
|
32
|
+
var import_path = __toESM(require("path"));
|
|
33
|
+
function fileExists(filePath) {
|
|
34
|
+
return import_fs.default.existsSync(filePath);
|
|
35
|
+
}
|
|
36
|
+
function dirExists(dirPath) {
|
|
37
|
+
try {
|
|
38
|
+
return import_fs.default.existsSync(dirPath) && import_fs.default.statSync(dirPath).isDirectory();
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function readJson(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
const content = import_fs.default.readFileSync(filePath, "utf-8");
|
|
46
|
+
return JSON.parse(content);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveCwd(...segments) {
|
|
52
|
+
return import_path.default.resolve(process.cwd(), ...segments);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/steps/detect.ts
|
|
56
|
+
function parseMajorVersion(versionStr) {
|
|
57
|
+
if (!versionStr) return 0;
|
|
58
|
+
const cleaned = versionStr.replace(/^[\^~>=<\s]+/, "");
|
|
59
|
+
const major = parseInt(cleaned.split(".")[0], 10);
|
|
60
|
+
return isNaN(major) ? 0 : major;
|
|
61
|
+
}
|
|
62
|
+
function detectProject() {
|
|
63
|
+
const hasPackageJson = fileExists(resolveCwd("package.json"));
|
|
64
|
+
const pkg = hasPackageJson ? readJson(resolveCwd("package.json")) : null;
|
|
65
|
+
const deps = {
|
|
66
|
+
...pkg?.dependencies ?? {},
|
|
67
|
+
...pkg?.devDependencies ?? {}
|
|
68
|
+
};
|
|
69
|
+
const hasNext = "next" in deps;
|
|
70
|
+
const nextVersion = typeof deps["next"] === "string" ? deps["next"] : null;
|
|
71
|
+
const hasZod = "zod" in deps;
|
|
72
|
+
const hasSrcApp = dirExists(resolveCwd("src", "app"));
|
|
73
|
+
const hasRootApp = dirExists(resolveCwd("app"));
|
|
74
|
+
const hasAppRouter = hasSrcApp || hasRootApp;
|
|
75
|
+
const srcDir = hasSrcApp && !hasRootApp;
|
|
76
|
+
const hasTypeScript = fileExists(resolveCwd("tsconfig.json"));
|
|
77
|
+
const packageManager = fileExists(
|
|
78
|
+
resolveCwd("pnpm-lock.yaml")
|
|
79
|
+
) ? "pnpm" : fileExists(resolveCwd("yarn.lock")) ? "yarn" : "npm";
|
|
80
|
+
return {
|
|
81
|
+
hasPackageJson,
|
|
82
|
+
hasNext,
|
|
83
|
+
hasAppRouter,
|
|
84
|
+
hasTypeScript,
|
|
85
|
+
hasZod,
|
|
86
|
+
packageManager,
|
|
87
|
+
nextVersion,
|
|
88
|
+
srcDir
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function detectConflicts(targetDir, srcDir, nextMajorVersion) {
|
|
92
|
+
const authTsPath = srcDir ? resolveCwd("src", "auth.ts") : resolveCwd("auth.ts");
|
|
93
|
+
const middlewareFileName = nextMajorVersion >= 16 ? "proxy.ts" : "middleware.ts";
|
|
94
|
+
const middlewarePath = srcDir ? resolveCwd("src", middlewareFileName) : resolveCwd(middlewareFileName);
|
|
95
|
+
return {
|
|
96
|
+
authDirExists: dirExists(resolveCwd(targetDir)),
|
|
97
|
+
authTsExists: fileExists(authTsPath),
|
|
98
|
+
middlewareExists: fileExists(middlewarePath)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function detectTsConfigAlias() {
|
|
102
|
+
const tsconfigPath = resolveCwd("tsconfig.json");
|
|
103
|
+
if (!fileExists(tsconfigPath)) return "@/";
|
|
104
|
+
const tsconfig = readJson(tsconfigPath);
|
|
105
|
+
if (!tsconfig) return "@/";
|
|
106
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
107
|
+
if (!paths || typeof paths !== "object") return "@/";
|
|
108
|
+
const rootMappings = ["./*", "./src/*", "src/*"];
|
|
109
|
+
for (const [aliasPattern, targets] of Object.entries(paths)) {
|
|
110
|
+
if (!Array.isArray(targets)) continue;
|
|
111
|
+
const mapsToRoot = targets.some((t) => rootMappings.includes(t));
|
|
112
|
+
if (!mapsToRoot) continue;
|
|
113
|
+
if (aliasPattern.endsWith("*")) {
|
|
114
|
+
return aliasPattern.slice(0, -1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return "@/";
|
|
118
|
+
}
|
|
119
|
+
function detectExistingAuthDir() {
|
|
120
|
+
const markerFile = "index.ts";
|
|
121
|
+
const candidates = [
|
|
122
|
+
"lib/auth",
|
|
123
|
+
"src/lib/auth",
|
|
124
|
+
"app/lib/auth",
|
|
125
|
+
"src/app/lib/auth",
|
|
126
|
+
"auth",
|
|
127
|
+
"src/auth"
|
|
128
|
+
];
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
if (fileExists(resolveCwd(candidate, markerFile))) {
|
|
131
|
+
return candidate;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/steps/copy-files.ts
|
|
138
|
+
var import_path2 = __toESM(require("path"));
|
|
139
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
140
|
+
|
|
141
|
+
// src/utils/logger.ts
|
|
142
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
143
|
+
var logger = {
|
|
144
|
+
info: (msg) => console.log(import_picocolors.default.cyan(" \u2139 ") + msg),
|
|
145
|
+
success: (msg) => console.log(import_picocolors.default.green(" \u2714 ") + msg),
|
|
146
|
+
warn: (msg) => console.log(import_picocolors.default.yellow(" \u26A0 ") + msg),
|
|
147
|
+
error: (msg) => console.log(import_picocolors.default.red(" \u2716 ") + msg),
|
|
148
|
+
step: (msg) => console.log(import_picocolors.default.blue(" \u2192 ") + msg),
|
|
149
|
+
dim: (msg) => console.log(import_picocolors.default.gray(" " + msg)),
|
|
150
|
+
break: () => console.log(),
|
|
151
|
+
banner() {
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(
|
|
154
|
+
" " + import_picocolors.default.bold(import_picocolors.default.cyan("@ss/next-jwt-auth")) + import_picocolors.default.gray(" v0.1.0")
|
|
155
|
+
);
|
|
156
|
+
console.log(
|
|
157
|
+
import_picocolors.default.gray(" JWT authentication scaffolder for Next.js App Router")
|
|
158
|
+
);
|
|
159
|
+
console.log();
|
|
160
|
+
},
|
|
161
|
+
done() {
|
|
162
|
+
console.log();
|
|
163
|
+
console.log(" " + import_picocolors.default.bold(import_picocolors.default.green("\u2714 Auth scaffold complete!")));
|
|
164
|
+
console.log();
|
|
165
|
+
},
|
|
166
|
+
nextSteps(authDir, hasMiddleware) {
|
|
167
|
+
console.log(import_picocolors.default.bold(" Next steps:"));
|
|
168
|
+
console.log();
|
|
169
|
+
console.log(
|
|
170
|
+
" 1. Open " + import_picocolors.default.cyan("auth.ts") + " and implement your three adapter functions"
|
|
171
|
+
);
|
|
172
|
+
console.log(
|
|
173
|
+
" " + import_picocolors.default.gray("login() \xB7 refreshToken() \xB7 fetchUser()")
|
|
174
|
+
);
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(
|
|
177
|
+
" 2. Wrap your root layout with " + import_picocolors.default.cyan("<AuthProvider actions={auth.actions}>")
|
|
178
|
+
);
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(" 3. Use server helpers in Server Components:");
|
|
181
|
+
console.log(" " + import_picocolors.default.cyan(`import { auth } from "@/auth"`));
|
|
182
|
+
console.log(
|
|
183
|
+
" " + import_picocolors.default.gray(
|
|
184
|
+
"auth.getSession() \xB7 auth.requireSession() \xB7 auth.getUser()"
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
console.log();
|
|
188
|
+
if (hasMiddleware) {
|
|
189
|
+
console.log(
|
|
190
|
+
" 4. Edit " + import_picocolors.default.cyan("middleware.ts") + " to configure your protected and public routes"
|
|
191
|
+
);
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
console.log(
|
|
195
|
+
" " + import_picocolors.default.gray("Library installed at: ") + import_picocolors.default.cyan(authDir + "/")
|
|
196
|
+
);
|
|
197
|
+
console.log();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/steps/copy-files.ts
|
|
202
|
+
async function copyLibraryFiles(targetDir) {
|
|
203
|
+
const sourceDir = import_path2.default.join(__dirname, "files", "lib", "auth");
|
|
204
|
+
const destDir = resolveCwd(targetDir);
|
|
205
|
+
await import_fs_extra.default.ensureDir(destDir);
|
|
206
|
+
await import_fs_extra.default.copy(sourceDir, destDir, { overwrite: true });
|
|
207
|
+
logger.success(`Library files copied to ${targetDir}/`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/steps/generate-auth.ts
|
|
211
|
+
var import_fs_extra2 = __toESM(require("fs-extra"));
|
|
212
|
+
var import_path3 = __toESM(require("path"));
|
|
213
|
+
async function generateAuthFile(authDir, srcDir, alias = "@/") {
|
|
214
|
+
const importSegment = srcDir && authDir.startsWith("src/") ? authDir.slice("src/".length) : authDir;
|
|
215
|
+
const importPath = `${alias}${importSegment}`;
|
|
216
|
+
const authFilePath = srcDir ? resolveCwd("src", "auth.ts") : resolveCwd("auth.ts");
|
|
217
|
+
await import_fs_extra2.default.ensureDir(import_path3.default.dirname(authFilePath));
|
|
218
|
+
const content = `import { Auth } from "${importPath}";
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Extend SessionUser with your own fields via module augmentation.
|
|
222
|
+
*
|
|
223
|
+
* Any fields you add here will be fully typed everywhere in your app:
|
|
224
|
+
* - session.user.name in Server Components
|
|
225
|
+
* - session.user in useSession() on the client
|
|
226
|
+
* - session.user in middleware
|
|
227
|
+
*
|
|
228
|
+
* Example:
|
|
229
|
+
*
|
|
230
|
+
* declare module "${importPath}" {
|
|
231
|
+
* interface SessionUser {
|
|
232
|
+
* name: string;
|
|
233
|
+
* role: "admin" | "user";
|
|
234
|
+
* avatarUrl?: string;
|
|
235
|
+
* }
|
|
236
|
+
* }
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
export const auth = Auth({
|
|
240
|
+
adapter: {
|
|
241
|
+
/**
|
|
242
|
+
* Called when the user submits your login form.
|
|
243
|
+
* Receives whatever credentials object you pass to login().
|
|
244
|
+
* Must return { accessToken, refreshToken } or throw an Error.
|
|
245
|
+
*
|
|
246
|
+
* Example:
|
|
247
|
+
* const res = await fetch("https://your-api.com/auth/login", {
|
|
248
|
+
* method: "POST",
|
|
249
|
+
* headers: { "Content-Type": "application/json" },
|
|
250
|
+
* body: JSON.stringify(credentials),
|
|
251
|
+
* });
|
|
252
|
+
* if (!res.ok) throw new Error("Invalid credentials");
|
|
253
|
+
* return res.json(); // { accessToken: "...", refreshToken: "..." }
|
|
254
|
+
*/
|
|
255
|
+
async login(credentials) {
|
|
256
|
+
throw new Error("login() not implemented \u2014 edit auth.ts to connect your API");
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Called automatically when the access token is near expiry.
|
|
261
|
+
* Receives the current refresh token.
|
|
262
|
+
* Must return { accessToken, refreshToken } or throw an Error.
|
|
263
|
+
*
|
|
264
|
+
* Example:
|
|
265
|
+
* const res = await fetch("https://your-api.com/auth/refresh", {
|
|
266
|
+
* method: "POST",
|
|
267
|
+
* headers: { "Content-Type": "application/json" },
|
|
268
|
+
* body: JSON.stringify({ refreshToken }),
|
|
269
|
+
* });
|
|
270
|
+
* if (!res.ok) throw new Error("Session expired");
|
|
271
|
+
* return res.json(); // { accessToken: "...", refreshToken: "..." }
|
|
272
|
+
*/
|
|
273
|
+
async refreshToken(refreshToken) {
|
|
274
|
+
throw new Error("refreshToken() not implemented \u2014 edit auth.ts to connect your API");
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Called after login and refresh to hydrate the user object.
|
|
279
|
+
* Receives the fresh access token.
|
|
280
|
+
* Must return an object with at least { id: string, email: string }.
|
|
281
|
+
* Add extra fields by extending SessionUser above.
|
|
282
|
+
*
|
|
283
|
+
* Example:
|
|
284
|
+
* const res = await fetch("https://your-api.com/me", {
|
|
285
|
+
* headers: { Authorization: \`Bearer \${accessToken}\` },
|
|
286
|
+
* });
|
|
287
|
+
* if (!res.ok) throw new Error("Failed to fetch user");
|
|
288
|
+
* return res.json(); // { id: "1", email: "user@example.com", name: "..." }
|
|
289
|
+
*/
|
|
290
|
+
async fetchUser(accessToken) {
|
|
291
|
+
throw new Error("fetchUser() not implemented \u2014 edit auth.ts to connect your API");
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Optional: called on logout to invalidate the refresh token server-side.
|
|
296
|
+
* If omitted, only the httpOnly cookies are cleared on logout.
|
|
297
|
+
*
|
|
298
|
+
* Example:
|
|
299
|
+
* await fetch("https://your-api.com/auth/logout", {
|
|
300
|
+
* method: "POST",
|
|
301
|
+
* headers: { "Content-Type": "application/json" },
|
|
302
|
+
* body: JSON.stringify({ refreshToken: tokens.refreshToken }),
|
|
303
|
+
* });
|
|
304
|
+
*/
|
|
305
|
+
// async logout(tokens) {},
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* All configuration below is optional \u2014 defaults shown.
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
// Enable verbose debug logging in development
|
|
313
|
+
// debug: process.env.NODE_ENV === "development",
|
|
314
|
+
|
|
315
|
+
// cookies: {
|
|
316
|
+
// name: "auth-session", // Generates "auth-session.access" + "auth-session.refresh" cookies
|
|
317
|
+
// secure: true, // Defaults to true in production, false in development
|
|
318
|
+
// sameSite: "lax",
|
|
319
|
+
// path: "/",
|
|
320
|
+
// domain: undefined, // Set for cross-subdomain auth
|
|
321
|
+
// },
|
|
322
|
+
|
|
323
|
+
// refresh: {
|
|
324
|
+
// refreshThresholdSeconds: 120, // Refresh when < 2 minutes remain on the access token
|
|
325
|
+
// },
|
|
326
|
+
|
|
327
|
+
// pages: {
|
|
328
|
+
// signIn: "/login", // Where to redirect unauthenticated users
|
|
329
|
+
// afterSignIn: "/dashboard", // Where to redirect after successful login
|
|
330
|
+
// afterSignOut: "/", // Where to redirect after logout
|
|
331
|
+
// },
|
|
332
|
+
});
|
|
333
|
+
`;
|
|
334
|
+
await import_fs_extra2.default.writeFile(authFilePath, content, "utf-8");
|
|
335
|
+
const displayPath = srcDir ? "src/auth.ts" : "auth.ts";
|
|
336
|
+
logger.success(`Generated ${displayPath}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/steps/generate-middleware.ts
|
|
340
|
+
var import_fs_extra3 = __toESM(require("fs-extra"));
|
|
341
|
+
var import_path4 = __toESM(require("path"));
|
|
342
|
+
function parseMajorVersion2(versionStr) {
|
|
343
|
+
if (!versionStr) return 0;
|
|
344
|
+
const cleaned = versionStr.replace(/^[\^~>=<\s]+/, "");
|
|
345
|
+
const major = parseInt(cleaned.split(".")[0], 10);
|
|
346
|
+
return isNaN(major) ? 0 : major;
|
|
347
|
+
}
|
|
348
|
+
async function generateMiddlewareFile(srcDir, nextVersion, alias = "@/") {
|
|
349
|
+
const majorVersion = parseMajorVersion2(nextVersion);
|
|
350
|
+
const fileName = majorVersion >= 16 ? "proxy.ts" : "middleware.ts";
|
|
351
|
+
const fileDir = srcDir ? resolveCwd("src") : resolveCwd(".");
|
|
352
|
+
const filePath = import_path4.default.join(fileDir, fileName);
|
|
353
|
+
await import_fs_extra3.default.ensureDir(fileDir);
|
|
354
|
+
const authImport = srcDir ? `${alias}auth` : `./auth`;
|
|
355
|
+
const content = `import { NextRequest, NextResponse } from "next/server";
|
|
356
|
+
import { auth } from "${authImport}";
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* ${fileName} \u2014 Route protection via @ss/next-jwt-auth
|
|
360
|
+
*
|
|
361
|
+
* The middleware runs on every request matched by config.matcher below.
|
|
362
|
+
* It silently refreshes expired access tokens so sessions stay alive.
|
|
363
|
+
*
|
|
364
|
+
* HOW IT WORKS:
|
|
365
|
+
* 1. resolveAuth() reads cookies, verifies the access token, and refreshes
|
|
366
|
+
* it transparently if it is near expiry or expired.
|
|
367
|
+
* 2. session.isAuthenticated \u2014 true if a valid session exists.
|
|
368
|
+
* 3. session.response(base) \u2014 writes refreshed token cookies onto your response.
|
|
369
|
+
* Always wrap your final NextResponse with this.
|
|
370
|
+
* 4. session.redirect(url) \u2014 redirects and clears session cookies.
|
|
371
|
+
* Use this when sending unauthenticated users to the login page.
|
|
372
|
+
*
|
|
373
|
+
* \u2500\u2500\u2500 PROTECTING ROUTES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
374
|
+
*
|
|
375
|
+
* Uncomment and adapt the examples below to match your route structure.
|
|
376
|
+
* auth.matchesPath() supports wildcards: "/dashboard/:path*" matches all sub-routes.
|
|
377
|
+
*
|
|
378
|
+
* Example \u2014 redirect unauthenticated users away from protected routes:
|
|
379
|
+
*
|
|
380
|
+
* if (!session.isAuthenticated && auth.matchesPath(pathname, ["/dashboard/:path*", "/settings/:path*"])) {
|
|
381
|
+
* const loginUrl = new URL("/login", request.url);
|
|
382
|
+
* loginUrl.searchParams.set("callbackUrl", pathname);
|
|
383
|
+
* return session.redirect(loginUrl);
|
|
384
|
+
* }
|
|
385
|
+
*
|
|
386
|
+
* Example \u2014 redirect authenticated users away from public-only routes:
|
|
387
|
+
*
|
|
388
|
+
* if (session.isAuthenticated && auth.matchesPath(pathname, ["/login", "/register"])) {
|
|
389
|
+
* return session.response(NextResponse.redirect(new URL("/", request.url)));
|
|
390
|
+
* }
|
|
391
|
+
*
|
|
392
|
+
* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
const resolveAuth = auth.createMiddleware();
|
|
396
|
+
|
|
397
|
+
export default async function ${majorVersion >= 16 ? "proxy" : "middleware"}(request: NextRequest) {
|
|
398
|
+
const session = await resolveAuth(request);
|
|
399
|
+
const { pathname } = request.nextUrl;
|
|
400
|
+
|
|
401
|
+
// Add your route protection logic here.
|
|
402
|
+
// See the comments above for examples.
|
|
403
|
+
|
|
404
|
+
// Pass through \u2014 always wrap with session.response() to write refreshed cookies
|
|
405
|
+
return session.response(NextResponse.next());
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export const config = {
|
|
409
|
+
// Run on all routes except Next.js internals and static files
|
|
410
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
411
|
+
};
|
|
412
|
+
`;
|
|
413
|
+
await import_fs_extra3.default.writeFile(filePath, content, "utf-8");
|
|
414
|
+
const displayPath = srcDir ? `src/${fileName}` : fileName;
|
|
415
|
+
logger.success(`Generated ${displayPath}`);
|
|
416
|
+
return { fileName, filePath };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/steps/install-deps.ts
|
|
420
|
+
var import_child_process = require("child_process");
|
|
421
|
+
function installDeps(packageManager, packages) {
|
|
422
|
+
if (packages.length === 0) return;
|
|
423
|
+
const cmd = packageManager === "pnpm" ? `pnpm add ${packages.join(" ")}` : packageManager === "yarn" ? `yarn add ${packages.join(" ")}` : `npm install ${packages.join(" ")}`;
|
|
424
|
+
logger.step(`Installing dependencies: ${packages.join(", ")}`);
|
|
425
|
+
try {
|
|
426
|
+
(0, import_child_process.execSync)(cmd, { stdio: "inherit", cwd: process.cwd() });
|
|
427
|
+
logger.success(`Installed: ${packages.join(", ")}`);
|
|
428
|
+
} catch {
|
|
429
|
+
logger.error(`Auto-install failed. Run this manually:
|
|
430
|
+
|
|
431
|
+
${cmd}
|
|
432
|
+
`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/commands/init.ts
|
|
437
|
+
async function init() {
|
|
438
|
+
logger.banner();
|
|
439
|
+
const project = detectProject();
|
|
440
|
+
if (!project.hasPackageJson) {
|
|
441
|
+
logger.error(
|
|
442
|
+
"No package.json found in the current directory.\n Make sure you are running this command from your project root."
|
|
443
|
+
);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
if (!project.hasNext) {
|
|
447
|
+
logger.warn(
|
|
448
|
+
"Next.js was not found in your dependencies.\n This library requires Next.js 14+ with App Router."
|
|
449
|
+
);
|
|
450
|
+
logger.break();
|
|
451
|
+
}
|
|
452
|
+
if (!project.hasAppRouter) {
|
|
453
|
+
logger.warn(
|
|
454
|
+
"No app/ directory detected \u2014 App Router is required.\n If you are using src/app/, that is fine and will be auto-detected."
|
|
455
|
+
);
|
|
456
|
+
logger.break();
|
|
457
|
+
}
|
|
458
|
+
const nextMajor = parseMajorVersion(project.nextVersion);
|
|
459
|
+
const middlewareFileName = nextMajor >= 16 ? "proxy.ts" : "middleware.ts";
|
|
460
|
+
const alias = detectTsConfigAlias();
|
|
461
|
+
if (project.hasNext) {
|
|
462
|
+
const version = project.nextVersion ? import_picocolors2.default.gray(`(${project.nextVersion})`) : "";
|
|
463
|
+
logger.info(
|
|
464
|
+
`Detected: Next.js ${version} \xB7 ${project.hasAppRouter ? import_picocolors2.default.green("App Router \u2714") : import_picocolors2.default.yellow("App Router?")} \xB7 ${project.hasTypeScript ? import_picocolors2.default.green("TypeScript \u2714") : import_picocolors2.default.yellow("JavaScript")} \xB7 ${project.srcDir ? import_picocolors2.default.cyan("src/ layout") : "root layout"}`
|
|
465
|
+
);
|
|
466
|
+
logger.dim(`Package manager: ${project.packageManager}`);
|
|
467
|
+
logger.dim(
|
|
468
|
+
`Import alias: ${alias} ${alias !== "@/" ? import_picocolors2.default.yellow("(custom \u2014 detected from tsconfig.json)") : import_picocolors2.default.gray("(default)")} `
|
|
469
|
+
);
|
|
470
|
+
if (nextMajor >= 16) {
|
|
471
|
+
logger.dim(
|
|
472
|
+
`Next.js >= 16 detected \u2014 will generate proxy.ts instead of middleware.ts`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
logger.break();
|
|
476
|
+
}
|
|
477
|
+
const answers = await (0, import_prompts.default)(
|
|
478
|
+
[
|
|
479
|
+
{
|
|
480
|
+
type: "text",
|
|
481
|
+
name: "authDir",
|
|
482
|
+
message: "Where should the auth library be placed?",
|
|
483
|
+
initial: project.srcDir ? "src/lib/auth" : "lib/auth",
|
|
484
|
+
validate: (val) => val.trim().length > 0 ? true : "Path cannot be empty"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
type: "confirm",
|
|
488
|
+
name: "generateMiddleware",
|
|
489
|
+
message: `Generate ${middlewareFileName} with route protection scaffold?`,
|
|
490
|
+
initial: true
|
|
491
|
+
},
|
|
492
|
+
...project.hasZod ? [] : [
|
|
493
|
+
{
|
|
494
|
+
type: "confirm",
|
|
495
|
+
name: "installZod",
|
|
496
|
+
message: `Install zod? ${import_picocolors2.default.gray("(required peer dependency)")}`,
|
|
497
|
+
initial: true
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
],
|
|
501
|
+
{
|
|
502
|
+
onCancel: () => {
|
|
503
|
+
logger.break();
|
|
504
|
+
logger.error("Setup cancelled.");
|
|
505
|
+
logger.break();
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
if (project.hasZod) answers.installZod = false;
|
|
511
|
+
logger.break();
|
|
512
|
+
const conflicts = detectConflicts(
|
|
513
|
+
answers.authDir,
|
|
514
|
+
project.srcDir,
|
|
515
|
+
nextMajor
|
|
516
|
+
);
|
|
517
|
+
if (conflicts.authDirExists) {
|
|
518
|
+
const { overwrite } = await (0, import_prompts.default)({
|
|
519
|
+
type: "confirm",
|
|
520
|
+
name: "overwrite",
|
|
521
|
+
message: `${import_picocolors2.default.yellow(answers.authDir)}/ already exists. Overwrite?`,
|
|
522
|
+
initial: false
|
|
523
|
+
});
|
|
524
|
+
if (!overwrite) {
|
|
525
|
+
logger.error("Aborted \u2014 existing directory was not modified.");
|
|
526
|
+
process.exit(0);
|
|
527
|
+
}
|
|
528
|
+
logger.break();
|
|
529
|
+
}
|
|
530
|
+
if (conflicts.authTsExists) {
|
|
531
|
+
const { overwriteAuth } = await (0, import_prompts.default)({
|
|
532
|
+
type: "confirm",
|
|
533
|
+
name: "overwriteAuth",
|
|
534
|
+
message: `${import_picocolors2.default.yellow(project.srcDir ? "src/auth.ts" : "auth.ts")} already exists. Overwrite?`,
|
|
535
|
+
initial: false
|
|
536
|
+
});
|
|
537
|
+
answers.skipAuthTs = !overwriteAuth;
|
|
538
|
+
if (answers.skipAuthTs)
|
|
539
|
+
logger.warn("Skipping auth.ts \u2014 existing file preserved.");
|
|
540
|
+
logger.break();
|
|
541
|
+
}
|
|
542
|
+
if (answers.generateMiddleware && conflicts.middlewareExists) {
|
|
543
|
+
const displayName = project.srcDir ? `src/${middlewareFileName}` : middlewareFileName;
|
|
544
|
+
const { overwriteMiddleware } = await (0, import_prompts.default)({
|
|
545
|
+
type: "confirm",
|
|
546
|
+
name: "overwriteMiddleware",
|
|
547
|
+
message: `${import_picocolors2.default.yellow(displayName)} already exists. Overwrite?`,
|
|
548
|
+
initial: false
|
|
549
|
+
});
|
|
550
|
+
if (!overwriteMiddleware) {
|
|
551
|
+
answers.generateMiddleware = false;
|
|
552
|
+
logger.warn(`Skipping ${middlewareFileName} \u2014 existing file preserved.`);
|
|
553
|
+
logger.break();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
logger.info("Scaffolding auth library...");
|
|
557
|
+
logger.break();
|
|
558
|
+
await copyLibraryFiles(answers.authDir);
|
|
559
|
+
if (!answers.skipAuthTs) {
|
|
560
|
+
await generateAuthFile(answers.authDir, project.srcDir, alias);
|
|
561
|
+
}
|
|
562
|
+
if (answers.generateMiddleware) {
|
|
563
|
+
await generateMiddlewareFile(project.srcDir, project.nextVersion, alias);
|
|
564
|
+
}
|
|
565
|
+
if (answers.installZod) {
|
|
566
|
+
logger.break();
|
|
567
|
+
installDeps(project.packageManager, ["zod"]);
|
|
568
|
+
}
|
|
569
|
+
logger.done();
|
|
570
|
+
logger.nextSteps(
|
|
571
|
+
answers.authDir,
|
|
572
|
+
answers.generateMiddleware
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/commands/update.ts
|
|
577
|
+
var import_prompts2 = __toESM(require("prompts"));
|
|
578
|
+
var import_picocolors3 = __toESM(require("picocolors"));
|
|
579
|
+
var import_path5 = __toESM(require("path"));
|
|
580
|
+
var import_fs_extra4 = __toESM(require("fs-extra"));
|
|
581
|
+
function listFilesRecursive(dir) {
|
|
582
|
+
if (!import_fs_extra4.default.existsSync(dir)) return [];
|
|
583
|
+
const results = [];
|
|
584
|
+
function walk(current) {
|
|
585
|
+
const entries = import_fs_extra4.default.readdirSync(current, { withFileTypes: true });
|
|
586
|
+
for (const entry of entries) {
|
|
587
|
+
const fullPath = import_path5.default.join(current, entry.name);
|
|
588
|
+
if (entry.isDirectory()) {
|
|
589
|
+
walk(fullPath);
|
|
590
|
+
} else {
|
|
591
|
+
results.push(import_path5.default.relative(dir, fullPath));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
walk(dir);
|
|
596
|
+
return results.sort();
|
|
597
|
+
}
|
|
598
|
+
function diffSnapshots(beforeDir, afterDir, files) {
|
|
599
|
+
const added = [];
|
|
600
|
+
const removed = [];
|
|
601
|
+
const modified = [];
|
|
602
|
+
const afterFiles = new Set(listFilesRecursive(afterDir));
|
|
603
|
+
const beforeFiles = new Set(files);
|
|
604
|
+
for (const file of afterFiles) {
|
|
605
|
+
if (!beforeFiles.has(file)) {
|
|
606
|
+
added.push(file);
|
|
607
|
+
} else {
|
|
608
|
+
const beforeSize = import_fs_extra4.default.statSync(import_path5.default.join(beforeDir, file)).size;
|
|
609
|
+
const afterSize = import_fs_extra4.default.statSync(import_path5.default.join(afterDir, file)).size;
|
|
610
|
+
if (beforeSize !== afterSize) {
|
|
611
|
+
modified.push(file);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const file of beforeFiles) {
|
|
616
|
+
if (!afterFiles.has(file)) {
|
|
617
|
+
removed.push(file);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { added, removed, modified };
|
|
621
|
+
}
|
|
622
|
+
async function update(dryRun = false) {
|
|
623
|
+
logger.banner();
|
|
624
|
+
if (dryRun) {
|
|
625
|
+
logger.info("Dry run \u2014 no files will be written.");
|
|
626
|
+
} else {
|
|
627
|
+
logger.info("Updating scaffolded library files...");
|
|
628
|
+
}
|
|
629
|
+
logger.break();
|
|
630
|
+
const project = detectProject();
|
|
631
|
+
if (!project.hasPackageJson) {
|
|
632
|
+
logger.error(
|
|
633
|
+
"No package.json found. Make sure you are running this from your project root."
|
|
634
|
+
);
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
const detected = detectExistingAuthDir();
|
|
638
|
+
let authDir;
|
|
639
|
+
if (detected) {
|
|
640
|
+
logger.info(`Found existing installation at: ${import_picocolors3.default.cyan(detected + "/")}`);
|
|
641
|
+
logger.break();
|
|
642
|
+
if (!dryRun) {
|
|
643
|
+
const { confirm } = await (0, import_prompts2.default)({
|
|
644
|
+
type: "confirm",
|
|
645
|
+
name: "confirm",
|
|
646
|
+
message: `Update files in ${import_picocolors3.default.cyan(detected + "/")}?`,
|
|
647
|
+
initial: true
|
|
648
|
+
});
|
|
649
|
+
if (!confirm) {
|
|
650
|
+
logger.break();
|
|
651
|
+
logger.error("Update cancelled.");
|
|
652
|
+
process.exit(0);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
authDir = detected;
|
|
656
|
+
} else {
|
|
657
|
+
logger.warn(
|
|
658
|
+
"Could not auto-detect an existing installation.\n Common locations checked: lib/auth, src/lib/auth, auth, src/auth"
|
|
659
|
+
);
|
|
660
|
+
logger.break();
|
|
661
|
+
const { manualDir } = await (0, import_prompts2.default)(
|
|
662
|
+
{
|
|
663
|
+
type: "text",
|
|
664
|
+
name: "manualDir",
|
|
665
|
+
message: "Enter the path where the library is installed:",
|
|
666
|
+
initial: project.srcDir ? "src/lib/auth" : "lib/auth",
|
|
667
|
+
validate: (val) => {
|
|
668
|
+
if (!val.trim()) return "Path cannot be empty";
|
|
669
|
+
if (!import_fs_extra4.default.existsSync(resolveCwd(val))) {
|
|
670
|
+
return `Directory not found: ${val}`;
|
|
671
|
+
}
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
onCancel: () => {
|
|
677
|
+
logger.break();
|
|
678
|
+
logger.error("Update cancelled.");
|
|
679
|
+
process.exit(0);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
authDir = manualDir;
|
|
684
|
+
}
|
|
685
|
+
logger.break();
|
|
686
|
+
const destDir = resolveCwd(authDir);
|
|
687
|
+
const beforeFiles = listFilesRecursive(destDir);
|
|
688
|
+
const sourceDir = import_path5.default.join(__dirname, "files", "lib", "auth");
|
|
689
|
+
let added, removed, modified;
|
|
690
|
+
if (dryRun) {
|
|
691
|
+
({ added, removed, modified } = diffSnapshots(destDir, sourceDir, beforeFiles));
|
|
692
|
+
} else {
|
|
693
|
+
logger.step("Copying updated library files...");
|
|
694
|
+
const tempDir = import_path5.default.join(
|
|
695
|
+
require("os").tmpdir(),
|
|
696
|
+
`next-jwt-auth-backup-${Date.now()}`
|
|
697
|
+
);
|
|
698
|
+
await import_fs_extra4.default.copy(destDir, tempDir, { overwrite: true });
|
|
699
|
+
try {
|
|
700
|
+
await copyLibraryFiles(authDir);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
await import_fs_extra4.default.copy(tempDir, destDir, { overwrite: true });
|
|
703
|
+
await import_fs_extra4.default.remove(tempDir);
|
|
704
|
+
logger.error(
|
|
705
|
+
`Update failed \u2014 original files restored.
|
|
706
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
707
|
+
);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
({ added, removed, modified } = diffSnapshots(tempDir, destDir, beforeFiles));
|
|
711
|
+
await import_fs_extra4.default.remove(tempDir);
|
|
712
|
+
}
|
|
713
|
+
logger.break();
|
|
714
|
+
if (dryRun) {
|
|
715
|
+
logger.info("Dry run complete \u2014 no files were modified.");
|
|
716
|
+
} else {
|
|
717
|
+
logger.success("Library files updated!");
|
|
718
|
+
}
|
|
719
|
+
logger.break();
|
|
720
|
+
const totalChanges = added.length + removed.length + modified.length;
|
|
721
|
+
if (totalChanges === 0) {
|
|
722
|
+
logger.dim(
|
|
723
|
+
dryRun ? "No file changes \u2014 already up to date." : "No file changes \u2014 you were already on the latest version."
|
|
724
|
+
);
|
|
725
|
+
} else {
|
|
726
|
+
if (modified.length > 0) {
|
|
727
|
+
console.log(import_picocolors3.default.bold(dryRun ? " Would update:" : " Updated:"));
|
|
728
|
+
modified.forEach((f) => logger.dim(`${import_picocolors3.default.yellow("~")} ${f}`));
|
|
729
|
+
logger.break();
|
|
730
|
+
}
|
|
731
|
+
if (added.length > 0) {
|
|
732
|
+
console.log(import_picocolors3.default.bold(dryRun ? " Would add:" : " Added:"));
|
|
733
|
+
added.forEach((f) => logger.dim(`${import_picocolors3.default.green("+")} ${f}`));
|
|
734
|
+
logger.break();
|
|
735
|
+
}
|
|
736
|
+
if (removed.length > 0) {
|
|
737
|
+
console.log(import_picocolors3.default.bold(dryRun ? " Would remove:" : " Removed:"));
|
|
738
|
+
removed.forEach((f) => logger.dim(`${import_picocolors3.default.red("-")} ${f}`));
|
|
739
|
+
logger.break();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const authTsPath = project.srcDir ? "src/auth.ts" : "auth.ts";
|
|
743
|
+
if (!dryRun && fileExists(resolveCwd(authTsPath))) {
|
|
744
|
+
logger.dim(
|
|
745
|
+
`${import_picocolors3.default.green("\u2714")} ${authTsPath} preserved \u2014 your adapter was not modified.`
|
|
746
|
+
);
|
|
747
|
+
logger.break();
|
|
748
|
+
}
|
|
749
|
+
const alias = detectTsConfigAlias();
|
|
750
|
+
logger.dim(`Import alias in use: ${import_picocolors3.default.cyan(alias)}`);
|
|
751
|
+
logger.dim(
|
|
752
|
+
"If your import alias has changed since you ran init, re-run " + import_picocolors3.default.cyan("npx @ss/next-jwt-auth init") + " to regenerate auth.ts and middleware.ts."
|
|
753
|
+
);
|
|
754
|
+
logger.break();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/check.ts
|
|
758
|
+
var import_picocolors4 = __toESM(require("picocolors"));
|
|
759
|
+
var import_fs_extra5 = __toESM(require("fs-extra"));
|
|
760
|
+
function readFileSafe(filePath) {
|
|
761
|
+
try {
|
|
762
|
+
return import_fs_extra5.default.readFileSync(filePath, "utf-8");
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function printResult(result) {
|
|
768
|
+
const icon = result.status === "pass" ? import_picocolors4.default.green(" \u2714") : result.status === "warn" ? import_picocolors4.default.yellow(" \u26A0") : result.status === "fail" ? import_picocolors4.default.red(" \u2716") : import_picocolors4.default.gray(" \u2013");
|
|
769
|
+
const label = result.status === "pass" ? import_picocolors4.default.white(result.label) : result.status === "warn" ? import_picocolors4.default.yellow(result.label) : result.status === "fail" ? import_picocolors4.default.red(result.label) : import_picocolors4.default.gray(result.label);
|
|
770
|
+
console.log(`${icon} ${label}`);
|
|
771
|
+
if (result.detail) {
|
|
772
|
+
console.log(` ${import_picocolors4.default.gray(result.detail)}`);
|
|
773
|
+
}
|
|
774
|
+
if (result.hint) {
|
|
775
|
+
console.log(` ${import_picocolors4.default.cyan("\u2192")} ${import_picocolors4.default.gray(result.hint)}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function checkAuthDirExists(authDir) {
|
|
779
|
+
if (!authDir) {
|
|
780
|
+
return {
|
|
781
|
+
label: "Library installation not found",
|
|
782
|
+
status: "fail",
|
|
783
|
+
detail: "Checked: lib/auth, src/lib/auth, auth, src/auth",
|
|
784
|
+
hint: "Run: npx @ss/next-jwt-auth init"
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
label: `Library found at ${authDir}/`,
|
|
789
|
+
status: "pass"
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function checkAuthTsExists(srcDir) {
|
|
793
|
+
const authTsPath = srcDir ? resolveCwd("src", "auth.ts") : resolveCwd("auth.ts");
|
|
794
|
+
const displayPath = srcDir ? "src/auth.ts" : "auth.ts";
|
|
795
|
+
if (!fileExists(authTsPath)) {
|
|
796
|
+
return {
|
|
797
|
+
label: `${displayPath} not found`,
|
|
798
|
+
status: "fail",
|
|
799
|
+
hint: "Run: npx @ss/next-jwt-auth init (it generates auth.ts for you)"
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
label: `${displayPath} exists`,
|
|
804
|
+
status: "pass"
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function checkAdapterImplemented(srcDir) {
|
|
808
|
+
const authTsPath = srcDir ? resolveCwd("src", "auth.ts") : resolveCwd("auth.ts");
|
|
809
|
+
const displayPath = srcDir ? "src/auth.ts" : "auth.ts";
|
|
810
|
+
const content = readFileSafe(authTsPath);
|
|
811
|
+
if (!content) {
|
|
812
|
+
return {
|
|
813
|
+
label: "Adapter implementation check skipped",
|
|
814
|
+
status: "skip",
|
|
815
|
+
detail: `${displayPath} could not be read`
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const stubs = [
|
|
819
|
+
{ fn: "login()", marker: "login() not implemented" },
|
|
820
|
+
{ fn: "refreshToken()", marker: "refreshToken() not implemented" },
|
|
821
|
+
{ fn: "fetchUser()", marker: "fetchUser() not implemented" }
|
|
822
|
+
];
|
|
823
|
+
const unimplemented = stubs.filter(({ marker }) => content.includes(marker)).map(({ fn }) => fn);
|
|
824
|
+
if (unimplemented.length === 0) {
|
|
825
|
+
return {
|
|
826
|
+
label: "Adapter functions implemented",
|
|
827
|
+
status: "pass"
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
label: `Adapter stubs not yet implemented: ${unimplemented.join(", ")}`,
|
|
832
|
+
status: "warn",
|
|
833
|
+
hint: `Open ${displayPath} and replace the throw stubs with your API calls`
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function checkAuthProviderInLayout(srcDir) {
|
|
837
|
+
const layoutCandidates = srcDir ? [
|
|
838
|
+
resolveCwd("src", "app", "layout.tsx"),
|
|
839
|
+
resolveCwd("src", "app", "layout.ts")
|
|
840
|
+
] : [resolveCwd("app", "layout.tsx"), resolveCwd("app", "layout.ts")];
|
|
841
|
+
for (const layoutPath of layoutCandidates) {
|
|
842
|
+
const content = readFileSafe(layoutPath);
|
|
843
|
+
if (!content) continue;
|
|
844
|
+
if (content.includes("AuthProvider")) {
|
|
845
|
+
return {
|
|
846
|
+
label: "AuthProvider found in root layout",
|
|
847
|
+
status: "pass"
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
label: "AuthProvider not found in root layout",
|
|
852
|
+
status: "warn",
|
|
853
|
+
detail: `Found layout at ${layoutPath.replace(resolveCwd(), ".")} but AuthProvider is missing`,
|
|
854
|
+
hint: "Wrap your layout's children with <AuthProvider actions={auth.actions}>"
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
label: "Root layout check skipped",
|
|
859
|
+
status: "skip",
|
|
860
|
+
detail: "Could not find app/layout.tsx or src/app/layout.tsx"
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function checkMiddleware(srcDir, nextMajor) {
|
|
864
|
+
const fileName = nextMajor >= 16 ? "proxy.ts" : "middleware.ts";
|
|
865
|
+
const middlewarePath = srcDir ? resolveCwd("src", fileName) : resolveCwd(fileName);
|
|
866
|
+
const displayPath = srcDir ? `src/${fileName}` : fileName;
|
|
867
|
+
if (!fileExists(middlewarePath)) {
|
|
868
|
+
return {
|
|
869
|
+
label: `${displayPath} not found`,
|
|
870
|
+
status: "warn",
|
|
871
|
+
detail: "Without middleware, tokens won't be silently refreshed between page navigations",
|
|
872
|
+
hint: `Run: npx @ss/next-jwt-auth init and choose to generate ${fileName}`
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const content = readFileSafe(middlewarePath);
|
|
876
|
+
if (!content) {
|
|
877
|
+
return {
|
|
878
|
+
label: `${displayPath} exists (could not read)`,
|
|
879
|
+
status: "skip"
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const hasCreateMiddleware = content.includes("createMiddleware");
|
|
883
|
+
const hasMatcher = content.includes("matcher");
|
|
884
|
+
const hasSessionResponse = content.includes("session.response");
|
|
885
|
+
if (!hasCreateMiddleware) {
|
|
886
|
+
return {
|
|
887
|
+
label: `${displayPath} found but createMiddleware() is missing`,
|
|
888
|
+
status: "warn",
|
|
889
|
+
detail: "The middleware won't refresh tokens without calling auth.createMiddleware()",
|
|
890
|
+
hint: "Re-generate with: npx @ss/next-jwt-auth init"
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (!hasSessionResponse) {
|
|
894
|
+
return {
|
|
895
|
+
label: `${displayPath} found but session.response() is missing`,
|
|
896
|
+
status: "warn",
|
|
897
|
+
detail: "Token cookies won't be written back without wrapping your response",
|
|
898
|
+
hint: "Return session.response(NextResponse.next()) instead of NextResponse.next()"
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
if (!hasMatcher) {
|
|
902
|
+
return {
|
|
903
|
+
label: `${displayPath} found but config.matcher is missing`,
|
|
904
|
+
status: "warn",
|
|
905
|
+
hint: 'Add: export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] }'
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
label: `${displayPath} looks correct`,
|
|
910
|
+
status: "pass"
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function checkImportAlias(srcDir) {
|
|
914
|
+
const detectedAlias = detectTsConfigAlias();
|
|
915
|
+
const authTsPath = srcDir ? resolveCwd("src", "auth.ts") : resolveCwd("auth.ts");
|
|
916
|
+
const displayPath = srcDir ? "src/auth.ts" : "auth.ts";
|
|
917
|
+
const content = readFileSafe(authTsPath);
|
|
918
|
+
if (!content) {
|
|
919
|
+
return {
|
|
920
|
+
label: "Import alias check skipped",
|
|
921
|
+
status: "skip",
|
|
922
|
+
detail: `${displayPath} could not be read`
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const match = content.match(/from\s+["']([^"']+)["']/);
|
|
926
|
+
if (!match) {
|
|
927
|
+
return {
|
|
928
|
+
label: "Import alias check skipped",
|
|
929
|
+
status: "skip",
|
|
930
|
+
detail: "Could not parse import path from auth.ts"
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const usedAlias = match[1].replace(/lib\/auth.*$/, "").replace(/auth.*$/, "");
|
|
934
|
+
if (usedAlias === detectedAlias) {
|
|
935
|
+
return {
|
|
936
|
+
label: `Import alias matches tsconfig (${import_picocolors4.default.cyan(detectedAlias)})`,
|
|
937
|
+
status: "pass"
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
label: "Import alias mismatch",
|
|
942
|
+
status: "warn",
|
|
943
|
+
detail: `auth.ts uses "${usedAlias}" but tsconfig.json configures "${detectedAlias}"`,
|
|
944
|
+
hint: "Re-run: npx @ss/next-jwt-auth init to regenerate auth.ts with the correct alias"
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
async function check() {
|
|
948
|
+
logger.banner();
|
|
949
|
+
console.log(import_picocolors4.default.bold(" Checking your next-jwt-auth setup...\n"));
|
|
950
|
+
const project = detectProject();
|
|
951
|
+
if (!project.hasPackageJson) {
|
|
952
|
+
logger.error("No package.json found. Run from your project root.");
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
const nextMajor = parseInt(
|
|
956
|
+
(project.nextVersion ?? "0").replace(/^[\^~>=<\s]+/, "").split(".")[0],
|
|
957
|
+
10
|
|
958
|
+
);
|
|
959
|
+
const authDir = detectExistingAuthDir();
|
|
960
|
+
const results = [
|
|
961
|
+
checkAuthDirExists(authDir),
|
|
962
|
+
checkAuthTsExists(project.srcDir),
|
|
963
|
+
checkAdapterImplemented(project.srcDir),
|
|
964
|
+
checkAuthProviderInLayout(project.srcDir),
|
|
965
|
+
checkMiddleware(project.srcDir, isNaN(nextMajor) ? 0 : nextMajor),
|
|
966
|
+
checkImportAlias(project.srcDir)
|
|
967
|
+
];
|
|
968
|
+
results.forEach(printResult);
|
|
969
|
+
logger.break();
|
|
970
|
+
const passed = results.filter((r) => r.status === "pass").length;
|
|
971
|
+
const warned = results.filter((r) => r.status === "warn").length;
|
|
972
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
973
|
+
const skipped = results.filter((r) => r.status === "skip").length;
|
|
974
|
+
const parts = [];
|
|
975
|
+
if (passed) parts.push(import_picocolors4.default.green(`${passed} passed`));
|
|
976
|
+
if (warned)
|
|
977
|
+
parts.push(import_picocolors4.default.yellow(`${warned} warning${warned > 1 ? "s" : ""}`));
|
|
978
|
+
if (failed) parts.push(import_picocolors4.default.red(`${failed} failed`));
|
|
979
|
+
if (skipped) parts.push(import_picocolors4.default.gray(`${skipped} skipped`));
|
|
980
|
+
console.log(` ${parts.join(import_picocolors4.default.gray(" \xB7 "))}`);
|
|
981
|
+
logger.break();
|
|
982
|
+
if (failed > 0) {
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/commands/uninstall.ts
|
|
988
|
+
var import_prompts3 = __toESM(require("prompts"));
|
|
989
|
+
var import_picocolors5 = __toESM(require("picocolors"));
|
|
990
|
+
var import_fs_extra6 = __toESM(require("fs-extra"));
|
|
991
|
+
async function uninstall() {
|
|
992
|
+
logger.banner();
|
|
993
|
+
logger.info("Removing scaffolded auth library files...");
|
|
994
|
+
logger.break();
|
|
995
|
+
const project = detectProject();
|
|
996
|
+
if (!project.hasPackageJson) {
|
|
997
|
+
logger.error(
|
|
998
|
+
"No package.json found. Make sure you are running this from your project root."
|
|
999
|
+
);
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
const detected = detectExistingAuthDir();
|
|
1003
|
+
if (!detected) {
|
|
1004
|
+
logger.warn(
|
|
1005
|
+
"Could not find an existing installation.\n Common locations checked: lib/auth, src/lib/auth, auth, src/auth"
|
|
1006
|
+
);
|
|
1007
|
+
process.exit(0);
|
|
1008
|
+
}
|
|
1009
|
+
logger.info(`Found installation at: ${import_picocolors5.default.cyan(detected + "/")}`);
|
|
1010
|
+
logger.break();
|
|
1011
|
+
const nextMajor = parseMajorVersion(project.nextVersion);
|
|
1012
|
+
const middlewareFileName = nextMajor >= 16 ? "proxy.ts" : "middleware.ts";
|
|
1013
|
+
const authTsPath = project.srcDir ? "src/auth.ts" : "auth.ts";
|
|
1014
|
+
const middlewarePath = project.srcDir ? `src/${middlewareFileName}` : middlewareFileName;
|
|
1015
|
+
const authTsExists = fileExists(resolveCwd(authTsPath));
|
|
1016
|
+
const middlewareExists = fileExists(resolveCwd(middlewarePath));
|
|
1017
|
+
const { confirmDir } = await (0, import_prompts3.default)(
|
|
1018
|
+
{
|
|
1019
|
+
type: "confirm",
|
|
1020
|
+
name: "confirmDir",
|
|
1021
|
+
message: `Delete ${import_picocolors5.default.red(detected + "/")} (the library files)?`,
|
|
1022
|
+
initial: true
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
onCancel: () => {
|
|
1026
|
+
logger.break();
|
|
1027
|
+
logger.error("Uninstall cancelled.");
|
|
1028
|
+
process.exit(0);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
1032
|
+
let removeAuthTs = false;
|
|
1033
|
+
let removeMiddleware = false;
|
|
1034
|
+
if (authTsExists) {
|
|
1035
|
+
const { confirm } = await (0, import_prompts3.default)({
|
|
1036
|
+
type: "confirm",
|
|
1037
|
+
name: "confirm",
|
|
1038
|
+
message: `Delete ${import_picocolors5.default.red(authTsPath)} (your adapter config)?`,
|
|
1039
|
+
initial: false
|
|
1040
|
+
});
|
|
1041
|
+
removeAuthTs = confirm;
|
|
1042
|
+
}
|
|
1043
|
+
if (middlewareExists) {
|
|
1044
|
+
const { confirm } = await (0, import_prompts3.default)({
|
|
1045
|
+
type: "confirm",
|
|
1046
|
+
name: "confirm",
|
|
1047
|
+
message: `Delete ${import_picocolors5.default.red(middlewarePath)}?`,
|
|
1048
|
+
initial: false
|
|
1049
|
+
});
|
|
1050
|
+
removeMiddleware = confirm;
|
|
1051
|
+
}
|
|
1052
|
+
if (!confirmDir && !removeAuthTs && !removeMiddleware) {
|
|
1053
|
+
logger.break();
|
|
1054
|
+
logger.warn("Nothing to remove.");
|
|
1055
|
+
process.exit(0);
|
|
1056
|
+
}
|
|
1057
|
+
logger.break();
|
|
1058
|
+
if (confirmDir) {
|
|
1059
|
+
await import_fs_extra6.default.remove(resolveCwd(detected));
|
|
1060
|
+
logger.success(`Removed ${detected}/`);
|
|
1061
|
+
}
|
|
1062
|
+
if (removeAuthTs) {
|
|
1063
|
+
await import_fs_extra6.default.remove(resolveCwd(authTsPath));
|
|
1064
|
+
logger.success(`Removed ${authTsPath}`);
|
|
1065
|
+
}
|
|
1066
|
+
if (removeMiddleware) {
|
|
1067
|
+
await import_fs_extra6.default.remove(resolveCwd(middlewarePath));
|
|
1068
|
+
logger.success(`Removed ${middlewarePath}`);
|
|
1069
|
+
}
|
|
1070
|
+
logger.break();
|
|
1071
|
+
logger.dim("Uninstall complete.");
|
|
1072
|
+
logger.dim(
|
|
1073
|
+
"If you installed zod only for this library, you can remove it with your package manager."
|
|
1074
|
+
);
|
|
1075
|
+
logger.break();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/index.ts
|
|
1079
|
+
var args = process.argv.slice(2);
|
|
1080
|
+
var command = args[0];
|
|
1081
|
+
async function main() {
|
|
1082
|
+
if (!command || command === "init") {
|
|
1083
|
+
await init();
|
|
1084
|
+
} else if (command === "update") {
|
|
1085
|
+
const dryRun = args.includes("--dry-run");
|
|
1086
|
+
await update(dryRun);
|
|
1087
|
+
} else if (command === "check") {
|
|
1088
|
+
await check();
|
|
1089
|
+
} else if (command === "uninstall") {
|
|
1090
|
+
await uninstall();
|
|
1091
|
+
} else if (command === "--help" || command === "-h") {
|
|
1092
|
+
console.log("");
|
|
1093
|
+
console.log(
|
|
1094
|
+
" @ss/next-jwt-auth \u2014 JWT auth scaffolder for Next.js App Router"
|
|
1095
|
+
);
|
|
1096
|
+
console.log("");
|
|
1097
|
+
console.log(" Usage:");
|
|
1098
|
+
console.log(
|
|
1099
|
+
" npx @ss/next-jwt-auth init Scaffold auth into your project"
|
|
1100
|
+
);
|
|
1101
|
+
console.log(
|
|
1102
|
+
" npx @ss/next-jwt-auth update Update library files to the latest version"
|
|
1103
|
+
);
|
|
1104
|
+
console.log(
|
|
1105
|
+
" npx @ss/next-jwt-auth update --dry-run Preview changes without writing any files"
|
|
1106
|
+
);
|
|
1107
|
+
console.log(
|
|
1108
|
+
" npx @ss/next-jwt-auth check Validate your project setup (exits with code 1 on failure)"
|
|
1109
|
+
);
|
|
1110
|
+
console.log(
|
|
1111
|
+
" npx @ss/next-jwt-auth uninstall Remove scaffolded auth files from the project"
|
|
1112
|
+
);
|
|
1113
|
+
console.log("");
|
|
1114
|
+
} else if (command === "--version" || command === "-v") {
|
|
1115
|
+
console.log("0.1.0");
|
|
1116
|
+
} else {
|
|
1117
|
+
console.error(` Unknown command: ${command}`);
|
|
1118
|
+
console.log(" Run npx @ss/next-jwt-auth --help for usage");
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
main().catch((err) => {
|
|
1123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1124
|
+
console.error(`
|
|
1125
|
+
Fatal error: ${message}
|
|
1126
|
+
`);
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
});
|