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.
- package/package.json +1 -1
- package/templates/monorepo-starter/pnpm-lock.yaml +13375 -12441
- package/templates/monorepo-starter/server/.env.example +5 -2
- package/templates/monorepo-starter/server/package.json +100 -97
- package/templates/monorepo-starter/server/prisma/schema.prisma +47 -3
- package/templates/monorepo-starter/server/scripts/gen-soft-delete.ts +52 -0
- package/templates/monorepo-starter/server/src/app.module.ts +2 -5
- package/templates/monorepo-starter/server/src/lib/constants/app.ts +1 -0
- package/templates/monorepo-starter/server/src/lib/guards/auth.guard.ts +4 -2
- package/templates/monorepo-starter/server/src/lib/schemas/env.schema.ts +4 -1
- package/templates/monorepo-starter/server/src/lib/templates/notification.templates.ts +114 -81
- package/templates/monorepo-starter/server/src/modules/auth/auth.service.ts +10 -9
- package/templates/monorepo-starter/server/src/modules/logger/winston.config.ts +15 -1
- package/templates/monorepo-starter/server/src/modules/notification/nodemailer.service.ts +34 -0
- package/templates/monorepo-starter/server/src/modules/notification/notification.service.ts +17 -11
- package/templates/monorepo-starter/server/src/modules/prisma/prisma.extension.ts +5 -15
- package/templates/monorepo-starter/server/src/modules/prisma/soft-delete.models.ts +6 -0
- package/templates/monorepo-starter/server/src/modules/token/token.service.ts +3 -4
- package/templates/monorepo-starter/server/tsconfig.json +1 -0
- 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:#
|
|
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
|
|
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
|
|
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}!
|
|
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: `🔐
|
|
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
|
|
50
|
-
<p>If this wasn’t you, please <strong>reset your password</strong> immediately
|
|
51
|
-
<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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
138
|
+
subject: `📧 Verify Your Account`,
|
|
109
139
|
html: `
|
|
110
140
|
<div style="${baseStyles}">
|
|
111
|
-
<h2>
|
|
112
|
-
<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
|
-
|
|
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
|
|
165
|
+
subject: `📨 Confirm ${identifierType} Change — ${appName}`,
|
|
133
166
|
html: `
|
|
134
167
|
<div style="${baseStyles}">
|
|
135
|
-
<h2>
|
|
136
|
-
<p>We received a request to change your ${
|
|
137
|
-
<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
|
|
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},
|
|
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: `✅ ${
|
|
180
|
+
subject: `✅ ${identifierType} Changed Successfully — ${appName}`,
|
|
148
181
|
html: `
|
|
149
182
|
<div style="${baseStyles}">
|
|
150
|
-
<h2>
|
|
151
|
-
<p>Your ${
|
|
152
|
-
<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 ${
|
|
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
|
|
197
|
+
subject: `📲 Your 2FA Code`,
|
|
162
198
|
html: `
|
|
163
199
|
<div style="${baseStyles}">
|
|
164
|
-
<h2>
|
|
165
|
-
<p>Your
|
|
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
|
|
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
|
|
212
|
+
subject: `🔑 Enable 2FA — OTP Required`,
|
|
178
213
|
html: `
|
|
179
214
|
<div style="${baseStyles}">
|
|
180
|
-
<h2>
|
|
181
|
-
<p>Use
|
|
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
|
|
225
|
+
subject: `✅ 2FA Enabled`,
|
|
192
226
|
html: `
|
|
193
227
|
<div style="${baseStyles}">
|
|
194
|
-
<h2>
|
|
195
|
-
<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
|
|
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
|
|
240
|
+
subject: `🔑 Disable 2FA — OTP Required`,
|
|
208
241
|
html: `
|
|
209
242
|
<div style="${baseStyles}">
|
|
210
|
-
<h2>
|
|
211
|
-
<p>Use this code to disable 2FA
|
|
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
|
|
253
|
+
subject: `⚠️ 2FA Disabled`,
|
|
222
254
|
html: `
|
|
223
255
|
<div style="${baseStyles}">
|
|
224
|
-
<h2>
|
|
225
|
-
<p>Two-factor authentication has been disabled
|
|
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
|
|
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
|
|
267
|
+
subject: `⚠️ Security Alert`,
|
|
235
268
|
html: `
|
|
236
269
|
<div style="${baseStyles}">
|
|
237
|
-
<h2>
|
|
270
|
+
<h2>Hello ${user.displayName},</h2>
|
|
238
271
|
<p>${message}</p>
|
|
239
|
-
<p>If you
|
|
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.
|
|
64
|
+
await this.notifyService.sendNotification({
|
|
65
65
|
userId: newUser.id,
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
purpose: "signup",
|
|
67
|
+
to: value,
|
|
68
68
|
metadata: { user: newUser },
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
await this.
|
|
71
|
+
await this.otpService.sendOtp({
|
|
72
72
|
userId: newUser.id,
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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 =
|
|
61
|
-
await this.
|
|
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
|
-
|
|
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);
|
|
@@ -95,11 +95,10 @@ export class TokenService {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
async createAuthSession(req: Request, res: Response, user: Express.User) {
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
|