create-zhx-monorepo 0.2.2 → 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 (20) hide show
  1. package/package.json +1 -1
  2. package/templates/monorepo-starter/pnpm-lock.yaml +13375 -12441
  3. package/templates/monorepo-starter/server/.env.example +5 -2
  4. package/templates/monorepo-starter/server/package.json +100 -97
  5. package/templates/monorepo-starter/server/prisma/schema.prisma +47 -3
  6. package/templates/monorepo-starter/server/scripts/gen-soft-delete.ts +52 -0
  7. package/templates/monorepo-starter/server/src/app.module.ts +2 -5
  8. package/templates/monorepo-starter/server/src/lib/constants/app.ts +1 -0
  9. package/templates/monorepo-starter/server/src/lib/guards/auth.guard.ts +4 -2
  10. package/templates/monorepo-starter/server/src/lib/schemas/env.schema.ts +4 -1
  11. package/templates/monorepo-starter/server/src/lib/templates/notification.templates.ts +114 -81
  12. package/templates/monorepo-starter/server/src/modules/auth/auth.service.ts +10 -9
  13. package/templates/monorepo-starter/server/src/modules/logger/winston.config.ts +15 -1
  14. package/templates/monorepo-starter/server/src/modules/notification/nodemailer.service.ts +34 -0
  15. package/templates/monorepo-starter/server/src/modules/notification/notification.service.ts +17 -11
  16. package/templates/monorepo-starter/server/src/modules/prisma/prisma.extension.ts +5 -15
  17. package/templates/monorepo-starter/server/src/modules/prisma/soft-delete.models.ts +6 -0
  18. package/templates/monorepo-starter/server/src/modules/token/token.service.ts +3 -4
  19. package/templates/monorepo-starter/server/tsconfig.json +1 -0
  20. package/templates/monorepo-starter/server/tsup.config.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { EnvService } from "@modules/env/env.service";
2
2
  import type { Otp, User } from "@generated/prisma";
3
+ import { appName } from "@constants/app";
3
4
 
4
5
  const baseStyles = `
5
6
  font-family: Arial, sans-serif;
@@ -10,15 +11,13 @@ const baseStyles = `
10
11
  const buttonStyles = `
11
12
  display:inline-block;
12
13
  padding:12px 20px;
13
- background-color:#000;
14
+ background-color:#007bff;
14
15
  color:#fff;
15
16
  text-decoration:none;
16
17
  border-radius:6px;
17
18
  font-weight:bold;
18
19
  `;
19
20
 
20
- const appName = "Evorii";
21
-
22
21
  export interface TemplateProps {
23
22
  user: User;
24
23
  otp?: Otp;
@@ -28,75 +27,106 @@ export interface TemplateProps {
28
27
  message?: string;
29
28
  }
30
29
 
30
+ /** Sign up email */
31
31
  export const signupTemplate = ({ user }: TemplateProps) => ({
32
- subject: `🎉 Welcome to ${appName}, ${user.displayName}!`,
32
+ subject: `🎉 Welcome, ${user.displayName}!`,
33
33
  html: `
34
34
  <div style="${baseStyles}">
35
35
  <h1>Welcome to ${appName}, ${user.displayName}!</h1>
36
- <p>We’re thrilled to have you join our creative community. Explore thoughtful, customizable gifts designed to express what truly matters.</p>
37
- <p>If you ever need assistance, our support team is just an email away.</p>
36
+ <p>We’re glad to have you onboard. Explore and make the most of our platform.</p>
38
37
  <p>— The ${appName} Team</p>
39
38
  </div>
40
39
  `,
41
- text: `Welcome to ${appName}, ${user.displayName}! Let's create something meaningful together.`,
40
+ text: `Welcome to ${appName}, ${user.displayName}! Enjoy exploring the platform.`,
42
41
  });
43
42
 
43
+ /** Sign in email */
44
44
  export const signinTemplate = ({ user }: TemplateProps) => ({
45
- subject: `🔐 Login Successful ${appName}`,
45
+ subject: `🔐 You’ve signed in to ${appName}`,
46
46
  html: `
47
47
  <div style="${baseStyles}">
48
48
  <h2>Hi ${user.displayName},</h2>
49
- <p>You’ve successfully signed in to your ${appName} account.</p>
50
- <p>If this wasn’t you, please <strong>reset your password</strong> immediately from your account settings.</p>
51
- <p>Stay secure,<br/>The ${appName} Team</p>
49
+ <p>You’ve successfully signed in to your account.</p>
50
+ <p>If this wasn’t you, please <strong>reset your password</strong> immediately.</p>
51
+ <p>— The ${appName} Team</p>
52
52
  </div>
53
53
  `,
54
- text: `Hi ${user.displayName}, you’ve logged in to ${appName}. If this wasn’t you, reset your password right away.`,
54
+ text: `Hi ${user.displayName}, you’ve logged in. If this wasn’t you, reset your password immediately.`,
55
55
  });
56
56
 
57
+ /** Set password template (two-phase) */
57
58
  export const setPasswordTemplate = ({
58
59
  user,
59
60
  otp,
60
61
  identifier,
61
62
  env,
62
63
  }: TemplateProps) => {
63
- const link = `${env?.get("CLIENT_ENDPOINT")}/set-password?identifier=${identifier}&purpose=${otp?.purpose}&secret=${otp?.secret}&type=${otp?.type}`;
64
- return {
65
- subject: `🔐 Set Up Your ${appName} Password`,
66
- html: `
67
- <div style="${baseStyles}">
68
- <h2>Welcome, ${user.displayName}!</h2>
69
- <p>Let’s get you set up. Click the button below to create your password and complete your account setup.</p>
70
- <a href="${link}" style="${buttonStyles}">Set Password</a>
71
- <p>If you didn’t request this, you can safely ignore this email.</p>
72
- </div>
73
- `,
74
- text: `Hi ${user.displayName}, set your ${appName} password here: ${link}`,
75
- };
64
+ if (otp) {
65
+ const link = `${env?.get("CLIENT_ENDPOINT")}/set-password?identifier=${identifier}&purpose=${otp.purpose}&secret=${otp.secret}&type=${otp.type}`;
66
+ return {
67
+ subject: `🔐 Complete Account Setup`,
68
+ html: `
69
+ <div style="${baseStyles}">
70
+ <h2>Hello ${user.displayName},</h2>
71
+ <p>Use the code below to set your password and complete your account setup:</p>
72
+ <h3>${otp.secret}</h3>
73
+ <a href="${link}" style="${buttonStyles}">Set Password</a>
74
+ <p>If you didn’t request this, ignore this email.</p>
75
+ </div>
76
+ `,
77
+ text: `Hi ${user.displayName}, your OTP is ${otp.secret}. Set your password here: ${link}`,
78
+ };
79
+ } else {
80
+ return {
81
+ subject: `✅ Password Set Successfully`,
82
+ html: `
83
+ <div style="${baseStyles}">
84
+ <h2>Hello ${user.displayName},</h2>
85
+ <p>Your password has been successfully set. You can now log in to your account.</p>
86
+ </div>
87
+ `,
88
+ text: `Hi ${user.displayName}, your password has been set. You can now log in.`,
89
+ };
90
+ }
76
91
  };
77
92
 
93
+ /** Reset password template (two-phase) */
78
94
  export const resetPasswordTemplate = ({
79
95
  user,
80
96
  otp,
81
97
  identifier,
82
98
  env,
83
99
  }: TemplateProps) => {
84
- const link = `${env?.get("CLIENT_ENDPOINT")}/reset-password?identifier=${identifier}&purpose=${otp?.purpose}&secret=${otp?.secret}&type=${otp?.type}`;
85
- return {
86
- subject: `🔁 Reset Your ${appName} Password`,
87
- html: `
88
- <div style="${baseStyles}">
89
- <h2>Hi ${user.displayName},</h2>
90
- <p>We received a request to reset your ${appName} password for <strong>${identifier}</strong>.</p>
91
- <p>Your one-time code: <strong>${otp?.secret}</strong></p>
92
- <a href="${link}" style="${buttonStyles}">Reset Password</a>
93
- <p>If you didn’t request this change, please ignore this message.</p>
94
- </div>
95
- `,
96
- text: `Hi ${user.displayName}, your OTP: ${otp?.secret}. Reset your password here: ${link}`,
97
- };
100
+ if (otp) {
101
+ const link = `${env?.get("CLIENT_ENDPOINT")}/reset-password?identifier=${identifier}&purpose=${otp.purpose}&secret=${otp.secret}&type=${otp.type}`;
102
+ return {
103
+ subject: `🔁 Reset Your Password`,
104
+ html: `
105
+ <div style="${baseStyles}">
106
+ <h2>Hello ${user.displayName},</h2>
107
+ <p>We received a request to reset your password for <strong>${identifier}</strong>. Use the code below:</p>
108
+ <h3>${otp.secret}</h3>
109
+ <a href="${link}" style="${buttonStyles}">Reset Password</a>
110
+ <p>If you didn’t request this, ignore this email.</p>
111
+ </div>
112
+ `,
113
+ text: `Hi ${user.displayName}, your OTP to reset password is ${otp.secret}. Reset here: ${link}`,
114
+ };
115
+ } else {
116
+ return {
117
+ subject: `✅ Password Reset Successfully`,
118
+ html: `
119
+ <div style="${baseStyles}">
120
+ <h2>Hello ${user.displayName},</h2>
121
+ <p>Your password has been successfully reset. You can now log in to your account.</p>
122
+ </div>
123
+ `,
124
+ text: `Hi ${user.displayName}, your password has been reset. You can now log in.`,
125
+ };
126
+ }
98
127
  };
99
128
 
129
+ /** Verify identifier template */
100
130
  export const verifyIdentifierTemplate = ({
101
131
  user,
102
132
  otp,
@@ -105,11 +135,11 @@ export const verifyIdentifierTemplate = ({
105
135
  }: TemplateProps) => {
106
136
  const link = `${env?.get("CLIENT_ENDPOINT")}/verify?identifier=${identifier}&purpose=${otp?.purpose}&secret=${otp?.secret}&type=${otp?.type}`;
107
137
  return {
108
- subject: `📧 Verify Your ${appName} Account`,
138
+ subject: `📧 Verify Your Account`,
109
139
  html: `
110
140
  <div style="${baseStyles}">
111
- <h2>Hi ${user.displayName},</h2>
112
- <p>Welcome! Please verify your ${identifier?.includes("@") ? "email address" : "phone number"} to activate your ${appName} account.</p>
141
+ <h2>Hello ${user.displayName},</h2>
142
+ <p>Please verify your ${identifier?.includes("@") ? "email address" : "phone number"} to activate your account.</p>
113
143
  <p>Your verification code: <strong>${otp?.secret}</strong></p>
114
144
  <a href="${link}" style="${buttonStyles}">Verify Now</a>
115
145
  </div>
@@ -118,6 +148,7 @@ export const verifyIdentifierTemplate = ({
118
148
  };
119
149
  };
120
150
 
151
+ /** Change identifier template (two-phase) */
121
152
  export const changeIdentifierTemplate = ({
122
153
  user,
123
154
  otp,
@@ -125,46 +156,51 @@ export const changeIdentifierTemplate = ({
125
156
  newIdentifier,
126
157
  env,
127
158
  }: TemplateProps) => {
159
+ const identifierType = identifier?.includes("@")
160
+ ? "email address"
161
+ : "phone number";
128
162
  if (otp) {
129
- // OTP exists → request confirmation phase
130
- const link = `${env?.get("CLIENT_ENDPOINT")}/confirm-change?identifier=${newIdentifier}&purpose=${otp.purpose}&secret=${otp.secret}&type=${otp.type}`;
163
+ const link = `${env?.get("CLIENT_ENDPOINT")}/confirm-change?identifier=${identifier}&newIdentifier=${newIdentifier}&purpose=${otp.purpose}&secret=${otp.secret}&type=${otp.type}`;
131
164
  return {
132
- subject: `📨 Confirm Change Request — ${appName}`,
165
+ subject: `📨 Confirm ${identifierType} Change — ${appName}`,
133
166
  html: `
134
167
  <div style="${baseStyles}">
135
- <h2>Hi ${user.displayName},</h2>
136
- <p>We received a request to change your ${identifier?.includes("@") ? "email address" : "phone number"}.</p>
137
- <p>Your one-time code: <strong>${otp.secret}</strong></p>
168
+ <h2>Hello ${user.displayName},</h2>
169
+ <p>We received a request to change your ${identifierType}.</p>
170
+ <p>Previous ${identifierType}: <strong>${identifier}</strong></p>
171
+ <p>New ${identifierType}: <strong>${newIdentifier}</strong></p>
138
172
  <a href="${link}" style="${buttonStyles}">Confirm Change</a>
139
- <p>If you didn’t make this request, please secure your account immediately.</p>
173
+ <p>If you didn’t request this, ignore this email and secure your account.</p>
140
174
  </div>
141
175
  `,
142
- text: `Hi ${user.displayName}, OTP: ${otp.secret}. Confirm your change here: ${link}`,
176
+ text: `Hi ${user.displayName}, request to change ${identifierType} from ${identifier} to ${newIdentifier}. Confirm here: ${link}`,
143
177
  };
144
178
  } else {
145
- // No OTP → confirmation phase
146
179
  return {
147
- subject: `✅ ${identifier?.includes("@") ? "Email" : "Phone"} Changed Successfully — ${appName}`,
180
+ subject: `✅ ${identifierType} Changed Successfully — ${appName}`,
148
181
  html: `
149
182
  <div style="${baseStyles}">
150
- <h2>Hi ${user.displayName},</h2>
151
- <p>Your ${identifier?.includes("@") ? "email address" : "phone number"} has been successfully updated to <strong>${newIdentifier}</strong>.</p>
152
- <p>If this wasn’t you, please update your credentials immediately.</p>
183
+ <h2>Hello ${user.displayName},</h2>
184
+ <p>Your ${identifierType} has been successfully updated.</p>
185
+ <p>Previous ${identifierType}: <strong>${identifier}</strong></p>
186
+ <p>New ${identifierType}: <strong>${newIdentifier}</strong></p>
187
+ <p>If this wasn’t you, update your credentials immediately.</p>
153
188
  </div>
154
189
  `,
155
- text: `Hi ${user.displayName}, your ${identifier?.includes("@") ? "email" : "phone"} has been updated to ${newIdentifier}.`,
190
+ text: `Hi ${user.displayName}, your ${identifierType} was changed from ${identifier} to ${newIdentifier}.`,
156
191
  };
157
192
  }
158
193
  };
159
194
 
195
+ /** MFA Templates */
160
196
  export const verifyMfaTemplate = ({ user, otp }: TemplateProps) => ({
161
- subject: `📲 Your ${appName} 2FA Code`,
197
+ subject: `📲 Your 2FA Code`,
162
198
  html: `
163
199
  <div style="${baseStyles}">
164
- <h2>Hi ${user.displayName},</h2>
165
- <p>Your ${appName} two-factor authentication code is:</p>
200
+ <h2>Hello ${user.displayName},</h2>
201
+ <p>Your two-factor authentication code is:</p>
166
202
  <h3>${otp?.secret}</h3>
167
- <p>This code expires shortly — do not share it with anyone.</p>
203
+ <p>This code expires shortly — do not share it.</p>
168
204
  </div>
169
205
  `,
170
206
  text: `Hi ${user.displayName}, your 2FA code is ${otp?.secret}.`,
@@ -172,13 +208,12 @@ export const verifyMfaTemplate = ({ user, otp }: TemplateProps) => ({
172
208
 
173
209
  export const enableMfaTemplate = ({ user, otp }: TemplateProps) => {
174
210
  if (otp) {
175
- // OTP exists → request confirmation
176
211
  return {
177
- subject: `🔑 Enable 2FA — OTP Required (${appName})`,
212
+ subject: `🔑 Enable 2FA — OTP Required`,
178
213
  html: `
179
214
  <div style="${baseStyles}">
180
- <h2>Hi ${user.displayName},</h2>
181
- <p>Use the following code to enable 2FA for your ${appName} account:</p>
215
+ <h2>Hello ${user.displayName},</h2>
216
+ <p>Use this code to enable 2FA for your account:</p>
182
217
  <h3>${otp.secret}</h3>
183
218
  <p>It expires soon. Do not share this code.</p>
184
219
  </div>
@@ -186,29 +221,27 @@ export const enableMfaTemplate = ({ user, otp }: TemplateProps) => {
186
221
  text: `Hi ${user.displayName}, use OTP ${otp.secret} to enable 2FA.`,
187
222
  };
188
223
  } else {
189
- // No OTP → success message
190
224
  return {
191
- subject: `✅ 2FA Enabled — ${appName}`,
225
+ subject: `✅ 2FA Enabled`,
192
226
  html: `
193
227
  <div style="${baseStyles}">
194
- <h2>Hi ${user.displayName},</h2>
195
- <p>You’ve successfully enabled two-factor authentication on your ${appName} account. Great job strengthening your security!</p>
228
+ <h2>Hello ${user.displayName},</h2>
229
+ <p>Two-factor authentication has been enabled successfully. Your account is now more secure.</p>
196
230
  </div>
197
231
  `,
198
- text: `Hi ${user.displayName}, 2FA has been enabled on your account.`,
232
+ text: `Hi ${user.displayName}, 2FA has been enabled.`,
199
233
  };
200
234
  }
201
235
  };
202
236
 
203
237
  export const disableMfaTemplate = ({ user, otp }: TemplateProps) => {
204
238
  if (otp) {
205
- // OTP exists → confirmation needed
206
239
  return {
207
- subject: `🔑 Disable 2FA — OTP Required (${appName})`,
240
+ subject: `🔑 Disable 2FA — OTP Required`,
208
241
  html: `
209
242
  <div style="${baseStyles}">
210
- <h2>Hi ${user.displayName},</h2>
211
- <p>Use this code to disable 2FA on your ${appName} account:</p>
243
+ <h2>Hello ${user.displayName},</h2>
244
+ <p>Use this code to disable 2FA:</p>
212
245
  <h3>${otp.secret}</h3>
213
246
  <p>If this wasn’t you, secure your account immediately.</p>
214
247
  </div>
@@ -216,27 +249,27 @@ export const disableMfaTemplate = ({ user, otp }: TemplateProps) => {
216
249
  text: `Hi ${user.displayName}, your OTP to disable 2FA is ${otp.secret}.`,
217
250
  };
218
251
  } else {
219
- // No OTP → confirmation
220
252
  return {
221
- subject: `⚠️ 2FA Disabled — ${appName}`,
253
+ subject: `⚠️ 2FA Disabled`,
222
254
  html: `
223
255
  <div style="${baseStyles}">
224
- <h2>Hi ${user.displayName},</h2>
225
- <p>Two-factor authentication has been disabled on your ${appName} account. If this wasn’t you, please re-enable it immediately.</p>
256
+ <h2>Hello ${user.displayName},</h2>
257
+ <p>Two-factor authentication has been disabled. If this wasn’t you, re-enable it immediately.</p>
226
258
  </div>
227
259
  `,
228
- text: `Hi ${user.displayName}, 2FA has been disabled. If this wasn’t you, secure your account.`,
260
+ text: `Hi ${user.displayName}, 2FA has been disabled.`,
229
261
  };
230
262
  }
231
263
  };
232
264
 
265
+ /** Security alert template */
233
266
  export const securityAlertTemplate = ({ user, message }: TemplateProps) => ({
234
- subject: `⚠️ Security Alert — ${appName}`,
267
+ subject: `⚠️ Security Alert`,
235
268
  html: `
236
269
  <div style="${baseStyles}">
237
- <h2>Hi ${user.displayName},</h2>
270
+ <h2>Hello ${user.displayName},</h2>
238
271
  <p>${message}</p>
239
- <p>If you suspect any suspicious activity, please change your password immediately.</p>
272
+ <p>If you notice any suspicious activity, update your password immediately.</p>
240
273
  </div>
241
274
  `,
242
275
  text: `Hi ${user.displayName}, ${message}`,
@@ -61,17 +61,17 @@ export class AuthService {
61
61
  },
62
62
  });
63
63
 
64
- await this.otpService.sendOtp({
64
+ await this.notifyService.sendNotification({
65
65
  userId: newUser.id,
66
- identifier: value,
67
- purpose: "verifyIdentifier",
66
+ purpose: "signup",
67
+ to: value,
68
68
  metadata: { user: newUser },
69
69
  });
70
70
 
71
- await this.notifyService.sendNotification({
71
+ await this.otpService.sendOtp({
72
72
  userId: newUser.id,
73
- purpose: "signup",
74
- to: value,
73
+ identifier: value,
74
+ purpose: "verifyIdentifier",
75
75
  metadata: { user: newUser },
76
76
  });
77
77
 
@@ -203,7 +203,7 @@ export class AuthService {
203
203
  if (dto.purpose === "setPassword" && user.password) {
204
204
  throw new BadRequestException(
205
205
  "Password already set. Use resetPassword."
206
- );
206
+ ); // TODO i don't thing this check need
207
207
  } else if (dto.purpose === "resetPassword" && !user.password) {
208
208
  throw new BadRequestException("No password set. Use setPassword.");
209
209
  }
@@ -377,7 +377,7 @@ export class AuthService {
377
377
  }
378
378
 
379
379
  async changeIdentifierReq(dto: ChangeIdentifierDto) {
380
- const { user } = await this.findUserByIdentifier(dto.identifier);
380
+ const { user, value } = await this.findUserByIdentifier(dto.identifier);
381
381
 
382
382
  const {
383
383
  key: newKey,
@@ -408,7 +408,8 @@ export class AuthService {
408
408
  userId: user.id,
409
409
  identifier: newValue,
410
410
  purpose: dto.purpose,
411
- metadata: { user },
411
+ type: "token",
412
+ metadata: { user, identifier: value, newIdentifier: newValue },
412
413
  });
413
414
 
414
415
  this.logger.log("🔄 Identifier change requested", {
@@ -1,15 +1,20 @@
1
1
  import * as winston from "winston";
2
2
  import "winston-daily-rotate-file";
3
3
  import { utilities } from "nest-winston";
4
+ import { appName } from "@/lib/constants/app";
5
+
6
+ const isProduction = process.env.NODE_ENV === "production";
4
7
 
5
8
  export const winstonConfig = {
6
9
  transports: [
7
10
  new winston.transports.Console({
11
+ level: isProduction ? "info" : "debug",
8
12
  format: winston.format.combine(
9
13
  winston.format.timestamp(),
10
- utilities.format.nestLike("EcomApp", { prettyPrint: true })
14
+ utilities.format.nestLike(appName, { prettyPrint: true })
11
15
  ),
12
16
  }),
17
+
13
18
  new winston.transports.DailyRotateFile({
14
19
  dirname: "logs",
15
20
  filename: "app-%DATE%.log",
@@ -19,6 +24,7 @@ export const winstonConfig = {
19
24
  maxFiles: "14d",
20
25
  level: "info",
21
26
  }),
27
+
22
28
  new winston.transports.DailyRotateFile({
23
29
  dirname: "logs",
24
30
  filename: "error-%DATE%.log",
@@ -29,4 +35,12 @@ export const winstonConfig = {
29
35
  level: "error",
30
36
  }),
31
37
  ],
38
+
39
+ exceptionHandlers: [
40
+ new winston.transports.File({ filename: "logs/exceptions.log" }),
41
+ ],
42
+
43
+ rejectionHandlers: [
44
+ new winston.transports.File({ filename: "logs/rejections.log" }),
45
+ ],
32
46
  };
@@ -0,0 +1,34 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { createTransport, type Transporter } from "nodemailer";
3
+ import { EnvService } from "@modules/env/env.service";
4
+
5
+ @Injectable()
6
+ export class NodemailerService {
7
+ private transporter: Transporter;
8
+
9
+ constructor(private readonly env: EnvService) {
10
+ this.transporter = createTransport({
11
+ host: this.env.get("SMTP_HOST"),
12
+ port: Number(this.env.get("SMTP_PORT")),
13
+ secure: this.env.get("SMTP_PORT") === 465,
14
+ auth: {
15
+ user: this.env.get("SMTP_USER"),
16
+ pass: this.env.get("SMTP_PASS"),
17
+ },
18
+ });
19
+ }
20
+
21
+ async sendMail(options: {
22
+ from: string;
23
+ to: string;
24
+ subject: string;
25
+ html: string;
26
+ }) {
27
+ try {
28
+ const info = await this.transporter.sendMail(options);
29
+ return info;
30
+ } catch (error) {
31
+ throw error;
32
+ }
33
+ }
34
+ }
@@ -1,11 +1,16 @@
1
1
  import { Injectable } from "@nestjs/common";
2
- import { Resend } from "resend";
2
+ import {
3
+ NotificationStatus,
4
+ NotificationType,
5
+ Prisma,
6
+ } from "@generated/prisma";
3
7
  import { EnvService } from "@modules/env/env.service";
4
- import { NotificationStatus, NotificationType, Prisma } from "@generated/prisma";
5
8
  import { PrismaService } from "@modules/prisma/prisma.service";
6
9
  import { TemplateService } from "@modules/template/template.service";
7
10
  import { LoggerService } from "@modules/logger/logger.service";
11
+ import { NodemailerService } from "./nodemailer.service";
8
12
  import { InjectLogger } from "@decorators/logger.decorator";
13
+ import { appName } from "@constants/app";
9
14
 
10
15
  interface SendNotificationProps {
11
16
  userId: string;
@@ -18,15 +23,13 @@ interface SendNotificationProps {
18
23
  export class NotificationService {
19
24
  @InjectLogger()
20
25
  private readonly logger!: LoggerService;
21
- private readonly resend: Resend;
22
26
 
23
27
  constructor(
24
- private readonly env: EnvService,
25
28
  private readonly prisma: PrismaService,
26
- private readonly templateService: TemplateService
27
- ) {
28
- this.resend = new Resend(this.env.get("RESEND_API_KEY"));
29
- }
29
+ private readonly env: EnvService,
30
+ private readonly templateService: TemplateService,
31
+ private readonly emailService: NodemailerService
32
+ ) {}
30
33
 
31
34
  async sendNotification({
32
35
  userId,
@@ -57,11 +60,14 @@ export class NotificationService {
57
60
  });
58
61
 
59
62
  if (type === "email") {
60
- const from = "Your App <onboarding@resend.dev>";
61
- await this.resend.emails.send({ from, to, subject, html });
63
+ const from = `${appName} <${this.env.get("SMTP_USER")}>`;
64
+ await this.emailService.sendMail({ from, to, subject, html });
62
65
  } else if (type === "sms") {
63
66
  // TODO integrate Twilio/Nexmo here
64
- console.log(`Sending SMS to ${to}: ${purpose} with data`, metadata);
67
+ this.logger.warn(
68
+ `Sending SMS to ${to}: ${purpose} with data`,
69
+ metadata
70
+ );
65
71
  }
66
72
 
67
73
  await this.updateNotificationStatus(notification.id, "sent");
@@ -1,10 +1,7 @@
1
1
  import { Prisma } from "@generated/prisma";
2
+ import { SoftDeleteModels } from "./soft-delete.models";
2
3
 
3
- const hasDeletedAt = (model: string) => {
4
- const dmmf = Prisma.dmmf.datamodel.models;
5
- const m = dmmf.find((m) => m.name === model);
6
- return !!m?.fields.find((f) => f.name === "deletedAt");
7
- };
4
+ const hasDeletedAt = (model: string) => SoftDeleteModels.has(model);
8
5
 
9
6
  export const softDeleteExtension = Prisma.defineExtension({
10
7
  name: "softDelete",
@@ -17,7 +14,6 @@ export const softDeleteExtension = Prisma.defineExtension({
17
14
 
18
15
  if (hasDel && !force) {
19
16
  const prisma = Prisma.getExtensionContext(this);
20
- console.log("prisma", prisma);
21
17
  return (prisma as any)[model].update({
22
18
  where: args.where,
23
19
  data: { deletedAt: new Date() },
@@ -33,7 +29,6 @@ export const softDeleteExtension = Prisma.defineExtension({
33
29
 
34
30
  if (hasDel && !force) {
35
31
  const prisma = Prisma.getExtensionContext(this);
36
- console.log("prisma", prisma);
37
32
  return (prisma as any)[model].updateMany({
38
33
  where: args.where,
39
34
  data: { deletedAt: new Date() },
@@ -45,16 +40,11 @@ export const softDeleteExtension = Prisma.defineExtension({
45
40
 
46
41
  async $allOperations({ model, operation, args, query }) {
47
42
  const hasDel = hasDeletedAt(model);
48
- const _args = args as Record<string, any>;
49
- const targetOps = [
50
- "findUnique",
51
- "findFirst",
52
- "findMany",
53
- "count",
54
- "aggregate",
55
- ];
43
+ const _args = { ...(args as Record<string, any>) };
44
+ const targetOps = ["findFirst", "findMany", "count", "aggregate"];
56
45
 
57
46
  if (!targetOps.includes(operation) || !hasDel) return query(args);
47
+
58
48
  if (_args.includeDeleted) {
59
49
  delete _args.includeDeleted;
60
50
  return query(_args);
@@ -0,0 +1,6 @@
1
+ // AUTO-GENERATED FILE — DO NOT EDIT
2
+ // Generated from schema.prisma
3
+
4
+ export const SoftDeleteModels = new Set<string>([
5
+ "User"
6
+ ]);
@@ -95,11 +95,10 @@ export class TokenService {
95
95
  }
96
96
 
97
97
  async createAuthSession(req: Request, res: Response, user: Express.User) {
98
- const tokens = await this.generateTokens(req, {
99
- sub: user.id,
100
- roles: user.roles,
101
- });
98
+ const payload = { sub: user.id, roles: user.roles };
99
+ const tokens = await this.generateTokens(req, payload);
102
100
  this.setAuthCookies(res, tokens);
101
+ this.attachDecodedUser(req, payload);
103
102
  return tokens;
104
103
  }
105
104
 
@@ -15,6 +15,7 @@
15
15
  "@schemas/*": ["src/lib/schemas/*"],
16
16
  "@templates/*": ["src/lib/templates/*"],
17
17
  "@utils/*": ["src/lib/utils/*"],
18
+ "@constants/*": ["src/lib/constants/*"],
18
19
  "@generated/prisma": ["./prisma/generated/client"]
19
20
  }
20
21
  },
@@ -3,7 +3,7 @@ import { defineConfig } from "tsup";
3
3
  export default defineConfig({
4
4
  entry: ["src/main.ts"],
5
5
  outDir: "dist",
6
- target: "es2022",
6
+ target: "es2024",
7
7
  format: ["esm"],
8
8
  splitting: false,
9
9
  sourcemap: true,