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
@@ -1,124 +1,212 @@
1
1
  import { betterAuth } from "better-auth";
2
+ import { bearer, emailOTP } from "better-auth/plugins";
2
3
  {{#if combo == "prisma:express"}}
3
- import { sendEmail } from "../../shared/email/email-service";
4
- import {
5
- getPasswordResetEmailTemplate,
6
- getVerificationEmailTemplate,
7
- } from "../../shared/email/email-templates";
8
- import { prisma } from "../../database/prisma";
4
+ import { Role, UserStatus } from "@prisma/client";
5
+ import { envVars } from "../config/env";
6
+ import { sendEmail } from "../shared/utils/email";
7
+ import { prisma } from "../database/prisma";
9
8
  import { prismaAdapter } from "better-auth/adapters/prisma";
10
9
  {{/if}}
11
-
12
10
  {{#if combo == "prisma:nextjs"}}
13
- import { getPasswordResetEmailTemplate, getVerificationEmailTemplate } from "../service/email/email-templates";
11
+ import { Role, UserStatus } from "@prisma/client";
14
12
  import { sendEmail } from "../service/email/email-service";
15
13
  import { prisma } from "../database/prisma";
14
+ import { envVars } from "@/lib/env";
16
15
  import { prismaAdapter } from "better-auth/adapters/prisma";
17
16
  {{/if}}
18
-
19
17
  {{#if combo == "mongoose:express"}}
20
- import { sendEmail } from "../../shared/email/email-service";
21
- import {
22
- getPasswordResetEmailTemplate,
23
- getVerificationEmailTemplate,
24
- } from "../../shared/email/email-templates";
25
- import { mongoose } from "../../database/mongoose";
18
+ import { envVars } from "../config/env";
19
+ import { Role, UserStatus } from "../modules/auth/auth.constants";
20
+ import { sendEmail } from "../shared/utils/email";
21
+ import { getMongoClient, getMongoDb, mongoose } from "../database/mongoose";
26
22
  import { mongodbAdapter } from "better-auth/adapters/mongodb";
27
23
  {{/if}}
28
-
29
24
  {{#if combo == "mongoose:nextjs"}}
30
- import { getPasswordResetEmailTemplate, getVerificationEmailTemplate } from "../service/email/email-templates";
31
25
  import { sendEmail } from "../service/email/email-service";
32
- import { mongoose } from "../database/mongoose";
26
+ import { getMongoClient, getMongoDb, mongoose } from "../database/mongoose";
27
+ import { Role, UserStatus } from "@/lib/auth/auth-constants";
28
+ import { envVars } from "@/lib/env";
33
29
  import { mongodbAdapter } from "better-auth/adapters/mongodb";
34
30
  {{/if}}
35
31
 
36
- export async function initAuth() {
37
32
  {{#if database == "mongoose"}}
38
- const mongooseInstance = await mongoose();
39
- const client = mongooseInstance.connection.getClient();
40
- const db = client.db();
33
+ await mongoose();
34
+ const client = getMongoClient();
35
+ const db = getMongoDb();
36
+ const usersCollection = db.collection("user");
41
37
  {{/if}}
42
38
 
43
- return betterAuth({
39
+ export const auth = betterAuth({
44
40
  {{#if database == "prisma"}}
45
41
  database: prismaAdapter(prisma, {
46
42
  provider: "{{prismaProvider}}",
47
43
  }),
48
44
  {{/if}}
49
45
  {{#if database == "mongoose"}}
50
- database: mongodbAdapter(db, { client }),
46
+ database: mongodbAdapter(db, { client, transaction: false }),
51
47
  {{/if}}
52
- baseURL: process.env.BETTER_AUTH_URL,
53
- secret: process.env.BETTER_AUTH_SECRET,
54
- trustedOrigins: [process.env.APP_URL!],
55
- user: {
56
- additionalFields: {
57
- role: {
58
- type: "string",
59
- defaultValue: "USER",
60
- required: true,
61
- },
62
- },
63
- },
48
+ baseURL: envVars.BETTER_AUTH_URL,
49
+ secret: envVars.BETTER_AUTH_SECRET,
50
+ trustedOrigins: [
51
+ envVars.FRONTEND_URL || envVars.BETTER_AUTH_URL || "http://localhost:3000",
52
+ ],
64
53
  emailAndPassword: {
65
54
  enabled: true,
66
55
  requireEmailVerification: true,
67
- sendResetPassword: async ({ user, url }) => {
68
- const { html, text } = getPasswordResetEmailTemplate(user, url);
69
- await sendEmail({
70
- to: user.email,
71
- subject: "Reset Your Password",
72
- text,
73
- html,
74
- });
75
- },
76
56
  },
77
57
  socialProviders: {
78
58
  google: {
79
- accessType: "offline",
59
+ clientId: envVars.GOOGLE_CLIENT_ID as string,
60
+ clientSecret: envVars.GOOGLE_CLIENT_SECRET as string,
61
+ accessType: "offline",
80
62
  prompt: "select_account consent",
81
- clientId: process.env.GOOGLE_CLIENT_ID as string,
82
- clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
63
+ mapProfileToUser: () => {
64
+ return {
65
+ role: Role.USER,
66
+ status: UserStatus.ACTIVE,
67
+ needPasswordChange: false,
68
+ emailVerified: true,
69
+ isDeleted: false,
70
+ deletedAt: null,
71
+ };
72
+ },
83
73
  },
84
74
  },
85
75
  emailVerification: {
86
- sendVerificationEmail: async ({ user, url }) => {
87
- const { html, text } = getVerificationEmailTemplate(user, url);
88
- await sendEmail({
89
- to: user.email,
90
- subject: "Verify Your Email Address",
91
- text,
92
- html,
93
- });
94
- },
76
+ sendOnSignUp: true,
95
77
  sendOnSignIn: true,
78
+ autoSignInAfterVerification: true,
96
79
  },
97
- rateLimit: {
98
- window: 10,
99
- max: 100,
100
- },
101
- account: {
102
- accountLinking: {
103
- enabled: true,
104
- trustedProviders: ["google"],
80
+ user: {
81
+ additionalFields: {
82
+ role: {
83
+ type: "string",
84
+ required: true,
85
+ defaultValue: Role.USER,
86
+ },
87
+ status: {
88
+ type: "string",
89
+ required: true,
90
+ defaultValue: UserStatus.ACTIVE,
91
+ },
92
+ needPasswordChange: {
93
+ type: "boolean",
94
+ required: true,
95
+ defaultValue: false,
96
+ },
97
+ isDeleted: {
98
+ type: "boolean",
99
+ required: true,
100
+ defaultValue: false,
101
+ },
102
+ deletedAt: {
103
+ type: "date",
104
+ required: false,
105
+ defaultValue: null,
106
+ },
105
107
  },
106
108
  },
109
+ plugins: [
110
+ bearer(),
111
+ emailOTP({
112
+ overrideDefaultEmailVerification: true,
113
+ async sendVerificationOTP({ email, otp, type }) {
114
+ if (type === "email-verification") {
115
+ {{#if database == "prisma"}}
116
+ const user = await prisma.user.findUnique({
117
+ where: {
118
+ email,
119
+ },
120
+ });
121
+ {{/if}}
122
+ {{#if database == "mongoose"}}
123
+ const user = await usersCollection.findOne({ email });
124
+ {{/if}}
125
+
126
+ if (!user) {
127
+ console.error(
128
+ `User with email ${email} not found. Cannot send verification OTP.`,
129
+ );
130
+ return;
131
+ }
132
+
133
+ if (user && user.role === Role.SUPER_ADMIN) {
134
+ console.log(
135
+ `User with email ${email} is a super admin. Skipping sending verification OTP.`,
136
+ );
137
+ return;
138
+ }
139
+
140
+ if (user && !user.emailVerified) {
141
+ sendEmail({
142
+ to: email,
143
+ subject: "Verify your email",
144
+ templateName: "otp",
145
+ templateData: {
146
+ name: user.name,
147
+ otp,
148
+ },
149
+ });
150
+ }
151
+ } else if (type === "forget-password") {
152
+ {{#if database == "prisma"}}
153
+ const user = await prisma.user.findUnique({
154
+ where: {
155
+ email,
156
+ },
157
+ });
158
+ {{/if}}
159
+ {{#if database == "mongoose"}}
160
+ const user = await usersCollection.findOne({ email });
161
+ {{/if}}
162
+
163
+ if (user) {
164
+ sendEmail({
165
+ to: email,
166
+ subject: "Password Reset OTP",
167
+ templateName: "otp",
168
+ templateData: {
169
+ name: user.name,
170
+ otp,
171
+ },
172
+ });
173
+ }
174
+ }
175
+ },
176
+ expiresIn: 2 * 60, // 2 minutes in seconds
177
+ otpLength: 6,
178
+ }),
179
+ ],
107
180
  session: {
181
+ expiresIn: 60 * 60 * 60 * 24, // 1 day in seconds
182
+ updateAge: 60 * 60 * 60 * 24, // 1 day in seconds
108
183
  cookieCache: {
109
184
  enabled: true,
110
- maxAge: 60 * 60 * 24 * 7,
185
+ maxAge: 60 * 60 * 60 * 24, // 1 day in seconds
111
186
  },
112
- expiresIn: 60 * 60 * 24 * 7,
113
- updateAge: 60 * 60 * 24,
114
- cookieName: "better-auth.session_token",
115
- }
116
- })
117
- };
118
-
119
- export let auth: any = undefined;
120
-
121
- export async function setupAuth() {
122
- auth = await initAuth();
123
- return auth;
124
- }
187
+ },
188
+ redirectURLs: {
189
+ signIn: `${envVars.BETTER_AUTH_URL}/api/v1/auth/google/success`,
190
+ },
191
+ advanced: {
192
+ useSecureCookies: false,
193
+ cookies: {
194
+ state: {
195
+ attributes: {
196
+ sameSite: "none",
197
+ secure: true,
198
+ httpOnly: true,
199
+ path: "/",
200
+ },
201
+ },
202
+ sessionToken: {
203
+ attributes: {
204
+ sameSite: "none",
205
+ secure: true,
206
+ httpOnly: true,
207
+ path: "/",
208
+ },
209
+ },
210
+ },
211
+ },
212
+ });
@@ -0,0 +1,11 @@
1
+ export const Role = {
2
+ ADMIN: "ADMIN",
3
+ USER: "USER",
4
+ SUPER_ADMIN: "SUPER_ADMIN",
5
+ } as const;
6
+
7
+ export const UserStatus = {
8
+ ACTIVE: "ACTIVE",
9
+ BLOCKED: "BLOCKED",
10
+ DELETED: "DELETED",
11
+ } as const;
@@ -0,0 +1,51 @@
1
+ import status from "http-status";
2
+ import { getMongoDb, mongoose } from "../../database/mongoose";
3
+ import { AppError } from "../../shared/errors/app-error";
4
+
5
+ export type AuthUser = {
6
+ id: string;
7
+ role: string;
8
+ name: string;
9
+ email: string;
10
+ status?: string;
11
+ isDeleted?: boolean;
12
+ emailVerified?: boolean;
13
+ needPasswordChange?: boolean;
14
+ deletedAt?: Date | null;
15
+ };
16
+
17
+ export type AuthUserDocument = AuthUser & {
18
+ createdAt?: Date;
19
+ updatedAt?: Date;
20
+ };
21
+
22
+ export type AuthSessionDocument = {
23
+ token: string;
24
+ userId: string;
25
+ createdAt?: Date;
26
+ updatedAt?: Date;
27
+ expiresAt?: Date;
28
+ };
29
+
30
+ export const getAuthCollections = async () => {
31
+ await mongoose();
32
+
33
+ try {
34
+ const db = getMongoDb();
35
+
36
+ return {
37
+ users: db.collection<AuthUserDocument>("user"),
38
+ sessions: db.collection<AuthSessionDocument>("session"),
39
+ };
40
+ } catch {
41
+ throw new AppError(
42
+ status.INTERNAL_SERVER_ERROR,
43
+ "Auth database is not initialized",
44
+ );
45
+ }
46
+ };
47
+
48
+ export const deleteAuthUserById = async (id: string) => {
49
+ const { users } = await getAuthCollections();
50
+ await users.deleteOne({ id });
51
+ };
@@ -4,16 +4,20 @@
4
4
  {{#var defaultId = @default(cuid())}}
5
5
  {{/if}}
6
6
  model User {
7
- id String @id {{defaultId}}
8
- name String
9
- email String
10
- emailVerified Boolean @default(false)
11
- image String?
12
- createdAt DateTime @default(now())
13
- updatedAt DateTime @updatedAt
14
- sessions Session[]
15
- accounts Account[]
16
- role Role @default(USER)
7
+ id String @id {{defaultId}}
8
+ name String
9
+ email String
10
+ emailVerified Boolean @default(false)
11
+ role Role @default(USER)
12
+ status UserStatus @default(ACTIVE)
13
+ needPasswordChange Boolean @default(false)
14
+ isDeleted Boolean @default(false)
15
+ deletedAt DateTime?
16
+ image String?
17
+ createdAt DateTime @default(now())
18
+ updatedAt DateTime @updatedAt
19
+ sessions Session[]
20
+ accounts Account[]
17
21
 
18
22
  @@unique([email])
19
23
  @@map("user")
@@ -67,6 +71,13 @@ model Verification {
67
71
  }
68
72
 
69
73
  enum Role {
70
- USER
74
+ SUPER_ADMIN
71
75
  ADMIN
76
+ USER
77
+ }
78
+
79
+ enum UserStatus {
80
+ ACTIVE
81
+ BLOCKED
82
+ DELETED
72
83
  }
@@ -0,0 +1,70 @@
1
+ import nodemailer from "nodemailer";
2
+ {{#if framework == "express"}}
3
+ import ejs from "ejs";
4
+ import status from "http-status";
5
+ import path from "path";
6
+ import { envVars } from "../../config/env";
7
+ import { AppError } from "../errors/app-error";
8
+ {{/if}}
9
+ {{#if framework == "nextjs"}}
10
+ import { renderEmailTemplate } from "../email/otp-template";
11
+ import { envVars } from "../env";
12
+ {{/if}}
13
+
14
+ const transporter = nodemailer.createTransport({
15
+ host: envVars.EMAIL_SENDER.SMTP_HOST,
16
+ secure: true,
17
+ auth: {
18
+ user: envVars.EMAIL_SENDER.SMTP_USER,
19
+ pass: envVars.EMAIL_SENDER.SMTP_PASS,
20
+ },
21
+ port: Number(envVars.EMAIL_SENDER.SMTP_PORT),
22
+ });
23
+
24
+ interface SendEmailOptions {
25
+ to: string;
26
+ subject: string;
27
+ templateName: string;
28
+ templateData: Record<string, string | number | boolean | object>;
29
+ attachments?: {
30
+ filename: string;
31
+ content: Buffer | string;
32
+ contentType: string;
33
+ }[];
34
+ }
35
+
36
+ export const sendEmail = async ({
37
+ subject,
38
+ templateData,
39
+ templateName,
40
+ to,
41
+ attachments,
42
+ }: SendEmailOptions) => {
43
+ try {
44
+ {{#if framework == "express"}}
45
+ const templatePath = path.resolve(
46
+ process.cwd(),
47
+ `src/templates/${templateName}.ejs`,
48
+ );
49
+
50
+ const html = await ejs.renderFile(templatePath, templateData);
51
+ {{/if}}
52
+ {{#if framework == "nextjs"}}
53
+ const html = renderEmailTemplate(templateName, templateData);
54
+ {{/if}}
55
+
56
+ await transporter.sendMail({
57
+ from: envVars.EMAIL_SENDER.SMTP_FROM,
58
+ to: to,
59
+ subject: subject,
60
+ html: html,
61
+ attachments: attachments?.map((attachment) => ({
62
+ filename: attachment.filename,
63
+ content: attachment.content,
64
+ contentType: attachment.contentType,
65
+ })),
66
+ });
67
+ } catch {
68
+ throw new AppError(status.INTERNAL_SERVER_ERROR, "Failed to send email");
69
+ }
70
+ };