stackkit 0.2.8 → 0.3.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.
Files changed (77) hide show
  1. package/README.md +4 -0
  2. package/bin/stackkit.js +8 -5
  3. package/dist/cli/add.js +4 -9
  4. package/dist/cli/create.js +4 -9
  5. package/dist/cli/doctor.js +11 -32
  6. package/dist/lib/constants.js +3 -4
  7. package/dist/lib/conversion/js-conversion.js +20 -16
  8. package/dist/lib/discovery/installed-detection.js +28 -38
  9. package/dist/lib/discovery/module-discovery.d.ts +0 -15
  10. package/dist/lib/discovery/module-discovery.js +15 -50
  11. package/dist/lib/framework/framework-utils.d.ts +4 -5
  12. package/dist/lib/framework/framework-utils.js +38 -49
  13. package/dist/lib/fs/files.js +1 -1
  14. package/dist/lib/generation/code-generator.d.ts +13 -19
  15. package/dist/lib/generation/code-generator.js +159 -175
  16. package/dist/lib/generation/generator-utils.js +3 -15
  17. package/dist/lib/project/detect.js +11 -19
  18. package/dist/lib/utils/fs-helpers.d.ts +1 -1
  19. package/modules/auth/authjs/generator.json +16 -16
  20. package/modules/auth/better-auth/files/express/middlewares/authorize.ts +178 -40
  21. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +264 -0
  22. package/modules/auth/better-auth/files/express/modules/auth/auth.route.ts +27 -0
  23. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +537 -0
  24. package/modules/auth/better-auth/files/express/modules/auth/auth.type.ts +33 -0
  25. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +91 -0
  26. package/modules/auth/better-auth/files/express/templates/otp.ejs +87 -0
  27. package/modules/auth/better-auth/files/express/types/express.d.ts +6 -8
  28. package/modules/auth/better-auth/files/express/utils/cookie.ts +19 -0
  29. package/modules/auth/better-auth/files/express/utils/jwt.ts +34 -0
  30. package/modules/auth/better-auth/files/express/utils/token.ts +66 -0
  31. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +1 -1
  32. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +11 -1
  33. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +74 -0
  34. package/modules/auth/better-auth/files/shared/config/env.ts +117 -0
  35. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +1 -1
  36. package/modules/auth/better-auth/files/shared/lib/auth.ts +167 -79
  37. package/modules/auth/better-auth/files/shared/mongoose/auth/constants.ts +11 -0
  38. package/modules/auth/better-auth/files/shared/mongoose/auth/helper.ts +51 -0
  39. package/modules/auth/better-auth/files/shared/prisma/schema.prisma +22 -11
  40. package/modules/auth/better-auth/files/shared/utils/email.ts +70 -0
  41. package/modules/auth/better-auth/generator.json +162 -80
  42. package/modules/database/mongoose/files/lib/mongoose.ts +28 -3
  43. package/modules/database/mongoose/generator.json +18 -18
  44. package/modules/database/prisma/generator.json +44 -44
  45. package/package.json +2 -2
  46. package/templates/express/env.example +3 -2
  47. package/templates/express/eslint.config.mjs +7 -0
  48. package/templates/express/node_modules/.bin/acorn +17 -0
  49. package/templates/express/node_modules/.bin/eslint +17 -0
  50. package/templates/express/node_modules/.bin/tsc +17 -0
  51. package/templates/express/node_modules/.bin/tsserver +17 -0
  52. package/templates/express/node_modules/.bin/tsx +17 -0
  53. package/templates/express/package.json +12 -6
  54. package/templates/express/src/app.ts +15 -7
  55. package/templates/express/src/config/cors.ts +8 -7
  56. package/templates/express/src/config/env.ts +28 -5
  57. package/templates/express/src/config/logger.ts +2 -2
  58. package/templates/express/src/config/rate-limit.ts +2 -2
  59. package/templates/express/src/modules/health/health.controller.ts +13 -11
  60. package/templates/express/src/routes/index.ts +1 -6
  61. package/templates/express/src/server.ts +12 -12
  62. package/templates/express/src/shared/errors/app-error.ts +16 -0
  63. package/templates/express/src/shared/middlewares/error.middleware.ts +154 -12
  64. package/templates/express/src/shared/middlewares/not-found.middleware.ts +2 -1
  65. package/templates/express/src/shared/utils/catch-async.ts +11 -0
  66. package/templates/express/src/shared/utils/pagination.ts +6 -1
  67. package/templates/express/src/shared/utils/send-response.ts +25 -0
  68. package/templates/nextjs/lib/env.ts +19 -8
  69. package/modules/auth/better-auth/files/shared/lib/email/email-service.ts +0 -33
  70. package/modules/auth/better-auth/files/shared/lib/email/email-templates.ts +0 -89
  71. package/templates/express/eslint.config.cjs +0 -42
  72. package/templates/express/src/config/helmet.ts +0 -5
  73. package/templates/express/src/modules/health/health.service.ts +0 -6
  74. package/templates/express/src/shared/errors/error-codes.ts +0 -9
  75. package/templates/express/src/shared/logger/logger.ts +0 -20
  76. package/templates/express/src/shared/utils/async-handler.ts +0 -9
  77. package/templates/express/src/shared/utils/response.ts +0 -9
@@ -64,7 +64,7 @@ async function mergeModuleIntoGeneratorConfig(config, modulePath) {
64
64
  }
65
65
  }
66
66
  catch {
67
- // ignore invalid module.json
67
+ return config;
68
68
  }
69
69
  }
70
70
  return config;
@@ -74,7 +74,6 @@ async function mergeGeneratorIntoModuleMetadata(metadata, modulePath) {
74
74
  if (await fs.pathExists(generatorPath)) {
75
75
  try {
76
76
  const generator = await fs.readJson(generatorPath);
77
- // Process add-env operations to extract envVars
78
77
  if (generator.operations && Array.isArray(generator.operations)) {
79
78
  for (const operation of generator.operations) {
80
79
  if (operation.type === "add-env" && operation.envVars) {
@@ -90,11 +89,6 @@ async function mergeGeneratorIntoModuleMetadata(metadata, modulePath) {
90
89
  });
91
90
  }
92
91
  }
93
- }
94
- }
95
- // Collect dependencies/devDependencies from add-dependency operations
96
- if (generator.operations && Array.isArray(generator.operations)) {
97
- for (const operation of generator.operations) {
98
92
  if (operation.type === "add-dependency") {
99
93
  if (operation.dependencies) {
100
94
  metadata.dependencies = {
@@ -117,7 +111,7 @@ async function mergeGeneratorIntoModuleMetadata(metadata, modulePath) {
117
111
  }
118
112
  }
119
113
  catch {
120
- // ignore invalid generator.json
114
+ return metadata;
121
115
  }
122
116
  }
123
117
  return metadata;
@@ -129,13 +123,7 @@ function locateOperationSource(generatorType, generatorName, sourceRel) {
129
123
  const moduleBasePath = generatorType === "framework"
130
124
  ? path.join(templatesPath, generatorName)
131
125
  : path.join(modulesPath, generatorType, generatorName);
132
- const sourcePath = path.join(moduleBasePath, "files", sourceRel);
133
- try {
134
- return sourcePath;
135
- }
136
- catch {
137
- return null;
138
- }
126
+ return path.join(moduleBasePath, "files", sourceRel);
139
127
  }
140
128
  exports.default = {
141
129
  mergeModuleIntoGeneratorConfig,
@@ -8,16 +8,15 @@ exports.getRouterBasePath = getRouterBasePath;
8
8
  exports.getLibPath = getLibPath;
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const constants_1 = require("../constants");
11
12
  const installed_detection_1 = require("../discovery/installed-detection");
12
13
  const package_root_1 = require("../utils/package-root");
13
14
  async function detectProjectInfo(targetDir) {
14
- const packageJsonPath = path_1.default.join(targetDir, "package.json");
15
+ const packageJsonPath = path_1.default.join(targetDir, constants_1.FILE_NAMES.PACKAGE_JSON);
15
16
  if (!(await fs_extra_1.default.pathExists(packageJsonPath))) {
16
- throw new Error("No package.json found. This does not appear to be a Node.js project.");
17
+ throw new Error(constants_1.ERROR_MESSAGES.NO_PACKAGE_JSON);
17
18
  }
18
19
  const packageJson = await fs_extra_1.default.readJSON(packageJsonPath);
19
- // Detect framework by matching available templates' characteristic files
20
- // Framework is dynamic and driven by templates; keep as string for discovery
21
20
  let framework = "unknown";
22
21
  try {
23
22
  const templatesDir = path_1.default.join((0, package_root_1.getPackageRoot)(), "templates");
@@ -41,19 +40,17 @@ async function detectProjectInfo(targetDir) {
41
40
  bestMatch = { name: d, score };
42
41
  }
43
42
  catch {
44
- // ignore
43
+ continue;
45
44
  }
46
45
  }
47
46
  if (bestMatch && bestMatch.score > 0) {
48
- // Use the template folder name as the framework identifier
49
47
  framework = bestMatch.name;
50
48
  }
51
49
  }
52
50
  }
53
51
  catch {
54
- // fall back to dependency heuristics below
52
+ framework = "unknown";
55
53
  }
56
- // Fallback: simple dependency-based detection
57
54
  if (framework === "unknown") {
58
55
  const isNextJs = packageJson.dependencies?.next || packageJson.devDependencies?.next;
59
56
  const isExpress = packageJson.dependencies?.express || packageJson.devDependencies?.express;
@@ -71,7 +68,6 @@ async function detectProjectInfo(targetDir) {
71
68
  if (framework === "unknown") {
72
69
  throw new Error("Unsupported project type or unable to detect framework from templates.");
73
70
  }
74
- // Detect router type (only for Next.js)
75
71
  let router = "unknown";
76
72
  if (framework === "nextjs") {
77
73
  const appDirExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "app"));
@@ -79,14 +75,13 @@ async function detectProjectInfo(targetDir) {
79
75
  const srcAppDirExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "src", "app"));
80
76
  const srcPagesDirExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "src", "pages"));
81
77
  if (appDirExists || srcAppDirExists) {
82
- router = "app";
78
+ router = constants_1.ROUTER_TYPES.APP;
83
79
  }
84
80
  else if (pagesDirExists || srcPagesDirExists) {
85
- router = "pages";
81
+ router = constants_1.ROUTER_TYPES.PAGES;
86
82
  }
87
83
  }
88
- // Detect TypeScript vs JavaScript
89
- const tsconfigExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "tsconfig.json"));
84
+ const tsconfigExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, constants_1.FILE_NAMES.TSCONFIG_JSON));
90
85
  const jsconfigExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "jsconfig.json"));
91
86
  let language;
92
87
  if (tsconfigExists) {
@@ -98,10 +93,9 @@ async function detectProjectInfo(targetDir) {
98
93
  else {
99
94
  language = "ts";
100
95
  }
101
- // Detect package manager
102
- const yarnLockExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "yarn.lock"));
103
- const pnpmLockExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "pnpm-lock.yaml"));
104
- const bunLockExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, "bun.lockb"));
96
+ const yarnLockExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, constants_1.LOCK_FILES.yarn));
97
+ const pnpmLockExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, constants_1.LOCK_FILES.pnpm));
98
+ const bunLockExists = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, constants_1.LOCK_FILES.bun));
105
99
  let packageManager = "pnpm";
106
100
  if (pnpmLockExists) {
107
101
  packageManager = "pnpm";
@@ -112,8 +106,6 @@ async function detectProjectInfo(targetDir) {
112
106
  else if (bunLockExists) {
113
107
  packageManager = "bun";
114
108
  }
115
- // Detect installed modules by comparing project dependencies against
116
- // declared dependencies in `modules/*/generator.json` and `module.json`.
117
109
  const detectedAuth = await (0, installed_detection_1.detectAuthModules)(packageJson);
118
110
  const detectedDbs = await (0, installed_detection_1.detectDatabaseModules)(packageJson);
119
111
  const hasAuth = detectedAuth.length > 0;
@@ -7,6 +7,6 @@ export declare function writeFile(filePath: string, content: string): Promise<vo
7
7
  export declare function copyDir(source: string, destination: string, options?: CopyFilterFunction): Promise<void>;
8
8
  export declare function readDir(dirPath: string): Promise<string[]>;
9
9
  export declare function isDirectory(dirPath: string): Promise<boolean>;
10
- export declare function findFilesInDir(dirPath: string, predicate: (file: string) => boolean): Promise<string[]>;
10
+ export declare function findFilesInDir(dirPath: string, predicate: (fileName: string) => boolean): Promise<string[]>;
11
11
  export declare function getEnvFilePath(projectRoot: string): Promise<string | null>;
12
12
  export {};
@@ -3,11 +3,6 @@
3
3
  "type": "auth",
4
4
  "priority": 10,
5
5
  "operations": [
6
- {
7
- "type": "create-file",
8
- "source": "shared/lib/auth.ts",
9
- "destination": "server/auth/auth.ts"
10
- },
11
6
  {
12
7
  "type": "create-file",
13
8
  "source": "nextjs/api/auth/[...nextauth]/route.ts",
@@ -18,6 +13,11 @@
18
13
  "source": "nextjs/proxy.ts",
19
14
  "destination": "proxy.ts"
20
15
  },
16
+ {
17
+ "type": "create-file",
18
+ "source": "shared/lib/auth.ts",
19
+ "destination": "server/auth/auth.ts"
20
+ },
21
21
  {
22
22
  "type": "patch-file",
23
23
  "destination": "prisma/schema.prisma",
@@ -36,6 +36,17 @@
36
36
  "@auth/prisma-adapter": "^2.11.1"
37
37
  }
38
38
  },
39
+ {
40
+ "type": "add-dependency",
41
+ "dependencies": {
42
+ "bcryptjs": "^3.0.3",
43
+ "next-auth": "^5.0.0-beta.30",
44
+ "nodemailer": "^7.0.12"
45
+ },
46
+ "devDependencies": {
47
+ "@types/nodemailer": "^7.0.5"
48
+ }
49
+ },
39
50
  {
40
51
  "type": "add-env",
41
52
  "envVars": {
@@ -48,17 +59,6 @@
48
59
  "EMAIL_PASS": "",
49
60
  "EMAIL_FROM": "noreply@yourapp.com"
50
61
  }
51
- },
52
- {
53
- "type": "add-dependency",
54
- "dependencies": {
55
- "next-auth": "^5.0.0-beta.30",
56
- "bcryptjs": "^3.0.3",
57
- "nodemailer": "^7.0.12"
58
- },
59
- "devDependencies": {
60
- "@types/nodemailer": "^7.0.5"
61
- }
62
62
  }
63
63
  ]
64
64
  }
@@ -1,54 +1,192 @@
1
1
  import { NextFunction, Request, Response } from "express";
2
- import { auth } from "../../modules/auth/auth";
2
+ import status from "http-status";
3
+ import { envVars } from "../../config/env";
4
+ import { AppError } from "../errors/app-error";
5
+ import { cookieUtils } from "../utils/cookie";
6
+ import { jwtUtils } from "../utils/jwt";
7
+ {{#if database == "prisma"}}
8
+ import { Role, UserStatus } from "@prisma/client";
9
+ import { prisma } from "../../database/prisma";
10
+ {{/if}}
11
+ {{#if database == "mongoose"}}
12
+ import { Role, UserStatus } from "../../modules/auth/auth.constants";
13
+ import { getAuthCollections } from "../../modules/auth/auth.helper";
14
+ {{/if}}
3
15
 
4
- export enum UserRole {
5
- USER = "USER",
6
- ADMIN = "ADMIN"
7
- }
16
+ {{#if database == "prisma"}}
17
+ export const authorize = (...authRoles: Role[]) =>
18
+ {{/if}}
19
+ {{#if database == "mongoose"}}
20
+ type AuthRole = (typeof Role)[keyof typeof Role];
8
21
 
9
- const authorize = (...roles: UserRole[]) => {
10
- return async (req: Request, res: Response, next: NextFunction) => {
22
+ export const authorize = (...authRoles: AuthRole[]) =>
23
+ {{/if}}
24
+ async (req: Request, res: Response, next: NextFunction) => {
11
25
  try {
12
- // get user session
13
- const session = await auth?.api.getSession({
14
- headers: req.headers as any,
15
- });
16
-
17
- if (!session) {
18
- return res.status(401).json({
19
- success: false,
20
- message: "You are not authorized!",
21
- });
26
+ //Session Token Verification
27
+ const sessionToken = cookieUtils.getCookie(
28
+ req,
29
+ "better-auth.session_token",
30
+ );
31
+
32
+ if (!sessionToken) {
33
+ throw new Error("Unauthorized access! No session token provided.");
22
34
  }
23
35
 
24
- if (!session.user.emailVerified) {
25
- return res.status(403).json({
26
- success: false,
27
- message: "Email verification required. Please verify your email!",
36
+ if (sessionToken) {
37
+ {{#if database == "prisma"}}
38
+ const sessionExists = await prisma.session.findFirst({
39
+ where: {
40
+ token: sessionToken,
41
+ expiresAt: {
42
+ gt: new Date(),
43
+ },
44
+ },
45
+ include: {
46
+ user: true,
47
+ },
28
48
  });
49
+ {{/if}}
50
+ {{#if database == "mongoose"}}
51
+ const { sessions, users } = await getAuthCollections();
52
+ const sessionExists = await sessions.findOne({
53
+ token: sessionToken,
54
+ expiresAt: { $gt: new Date() },
55
+ });
56
+
57
+ const user = sessionExists
58
+ ? await users.findOne({
59
+ id: sessionExists.userId,
60
+ })
61
+ : null;
62
+ {{/if}}
63
+
64
+ {{#if database == "prisma"}}
65
+ if (sessionExists && sessionExists.user) {
66
+ const user = sessionExists.user;
67
+ {{/if}}
68
+ {{#if database == "mongoose"}}
69
+ if (sessionExists && user) {
70
+ {{/if}}
71
+
72
+ {{#if database == "prisma"}}
73
+ const now = new Date();
74
+ const expiresAt = new Date(sessionExists.expiresAt);
75
+ const createdAt = new Date(sessionExists.createdAt);
76
+
77
+ const sessionLifeTime = expiresAt.getTime() - createdAt.getTime();
78
+ const timeRemaining = expiresAt.getTime() - now.getTime();
79
+ const percentRemaining = (timeRemaining / sessionLifeTime) * 100;
80
+
81
+ if (percentRemaining < 20) {
82
+ res.setHeader("X-Session-Refresh", "true");
83
+ res.setHeader("X-Session-Expires-At", expiresAt.toISOString());
84
+ res.setHeader("X-Time-Remaining", timeRemaining.toString());
85
+
86
+ console.log("Session Expiring Soon!!");
87
+ }
88
+ {{/if}}
89
+ {{#if database == "mongoose"}}
90
+ if (sessionExists.expiresAt && sessionExists.createdAt) {
91
+ const now = new Date();
92
+ const expiresAt = new Date(sessionExists.expiresAt);
93
+ const createdAt = new Date(sessionExists.createdAt);
94
+
95
+ const sessionLifeTime = expiresAt.getTime() - createdAt.getTime();
96
+ const timeRemaining = expiresAt.getTime() - now.getTime();
97
+ const percentRemaining = (timeRemaining / sessionLifeTime) * 100;
98
+
99
+ if (percentRemaining < 20) {
100
+ res.setHeader("X-Session-Refresh", "true");
101
+ res.setHeader("X-Session-Expires-At", expiresAt.toISOString());
102
+ res.setHeader("X-Time-Remaining", timeRemaining.toString());
103
+
104
+ console.log("Session Expiring Soon!!");
105
+ }
106
+ }
107
+ {{/if}}
108
+
109
+ if (
110
+ user.status === UserStatus.BLOCKED ||
111
+ user.status === UserStatus.DELETED
112
+ ) {
113
+ throw new AppError(
114
+ status.UNAUTHORIZED,
115
+ "Unauthorized access! User is not active.",
116
+ );
117
+ }
118
+
119
+ if (user.isDeleted) {
120
+ throw new AppError(
121
+ status.UNAUTHORIZED,
122
+ "Unauthorized access! User is deleted.",
123
+ );
124
+ }
125
+
126
+ {{#if database == "prisma"}}
127
+ if (authRoles.length > 0 && !authRoles.includes(user.role)) {
128
+ {{/if}}
129
+ {{#if database == "mongoose"}}
130
+ if (authRoles.length > 0 && !authRoles.includes(user.role as AuthRole)) {
131
+ {{/if}}
132
+ throw new AppError(
133
+ status.FORBIDDEN,
134
+ "Forbidden access! You do not have permission to access this resource.",
135
+ );
136
+ }
137
+
138
+ req.user = {
139
+ id: user.id,
140
+ name: user.name,
141
+ email: user.email,
142
+ role: user.role,
143
+ };
144
+ }
145
+
146
+ const accessToken = cookieUtils.getCookie(req, "accessToken");
147
+
148
+ if (!accessToken) {
149
+ throw new AppError(
150
+ status.UNAUTHORIZED,
151
+ "Unauthorized access! No access token provided.",
152
+ );
153
+ }
29
154
  }
30
155
 
31
- req.user = {
32
- id: session.user.id,
33
- email: session.user.email,
34
- name: session.user.name,
35
- role: session.user.role as string,
36
- emailVerified: session.user.emailVerified,
37
- };
38
-
39
- if (roles.length && !roles.includes(req.user.role as UserRole)) {
40
- return res.status(403).json({
41
- success: false,
42
- message:
43
- "Forbidden! You don't have permission to access this resources!",
44
- });
156
+ //Access Token Verification
157
+ const accessToken = cookieUtils.getCookie(req, "accessToken");
158
+
159
+ if (!accessToken) {
160
+ throw new AppError(
161
+ status.UNAUTHORIZED,
162
+ "Unauthorized access! No access token provided.",
163
+ );
164
+ }
165
+
166
+ const verifiedToken = jwtUtils.verifyToken(
167
+ accessToken,
168
+ envVars.ACCESS_TOKEN_SECRET,
169
+ );
170
+
171
+ if (!verifiedToken.success) {
172
+ throw new AppError(
173
+ status.UNAUTHORIZED,
174
+ "Unauthorized access! Invalid access token.",
175
+ );
176
+ }
177
+
178
+ if (
179
+ authRoles.length > 0 &&
180
+ !authRoles.includes(verifiedToken.data!.role)
181
+ ) {
182
+ throw new AppError(
183
+ status.FORBIDDEN,
184
+ "Forbidden access! You do not have permission to access this resource.",
185
+ );
45
186
  }
46
187
 
47
188
  next();
48
- } catch (err) {
49
- next(err);
189
+ } catch (error: unknown) {
190
+ next(error);
50
191
  }
51
- };
52
192
  };
53
-
54
- export default authorize;