stackkit 0.2.7 → 0.2.9
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/README.md +91 -12
- package/dist/lib/fs/files.js +1 -1
- package/dist/lib/generation/code-generator.d.ts +2 -7
- package/dist/lib/generation/code-generator.js +120 -56
- package/dist/lib/utils/fs-helpers.d.ts +1 -1
- package/modules/auth/authjs/generator.json +16 -16
- package/modules/auth/better-auth/files/express/middlewares/authorize.ts +121 -41
- package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +257 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.interface.ts +23 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.route.ts +22 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +403 -0
- package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +91 -0
- package/modules/auth/better-auth/files/express/templates/otp.ejs +87 -0
- package/modules/auth/better-auth/files/express/types/express.d.ts +6 -8
- package/modules/auth/better-auth/files/express/utils/cookie.ts +19 -0
- package/modules/auth/better-auth/files/express/utils/jwt.ts +34 -0
- package/modules/auth/better-auth/files/express/utils/token.ts +66 -0
- package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +1 -1
- package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +11 -1
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +74 -0
- package/modules/auth/better-auth/files/shared/config/env.ts +113 -0
- package/modules/auth/better-auth/files/shared/lib/auth-client.ts +1 -1
- package/modules/auth/better-auth/files/shared/lib/auth.ts +151 -62
- package/modules/auth/better-auth/files/shared/prisma/schema.prisma +22 -11
- package/modules/auth/better-auth/files/shared/utils/email.ts +71 -0
- package/modules/auth/better-auth/generator.json +159 -81
- package/modules/database/mongoose/generator.json +18 -18
- package/modules/database/prisma/generator.json +44 -44
- package/package.json +1 -1
- package/templates/express/env.example +2 -2
- package/templates/express/eslint.config.mjs +7 -0
- package/templates/express/node_modules/.bin/acorn +17 -0
- package/templates/express/node_modules/.bin/eslint +17 -0
- package/templates/express/node_modules/.bin/tsc +17 -0
- package/templates/express/node_modules/.bin/tsserver +17 -0
- package/templates/express/node_modules/.bin/tsx +17 -0
- package/templates/express/package.json +12 -6
- package/templates/express/src/app.ts +15 -7
- package/templates/express/src/config/cors.ts +8 -7
- package/templates/express/src/config/env.ts +26 -5
- package/templates/express/src/config/logger.ts +2 -2
- package/templates/express/src/config/rate-limit.ts +2 -2
- package/templates/express/src/modules/health/health.controller.ts +13 -11
- package/templates/express/src/routes/index.ts +1 -6
- package/templates/express/src/server.ts +12 -12
- package/templates/express/src/shared/errors/app-error.ts +16 -0
- package/templates/express/src/shared/middlewares/error.middleware.ts +154 -12
- package/templates/express/src/shared/middlewares/not-found.middleware.ts +2 -1
- package/templates/express/src/shared/utils/catch-async.ts +11 -0
- package/templates/express/src/shared/utils/pagination.ts +6 -1
- package/templates/express/src/shared/utils/send-response.ts +25 -0
- package/templates/nextjs/lib/env.ts +19 -8
- package/modules/auth/better-auth/files/shared/lib/email/email-service.ts +0 -33
- package/modules/auth/better-auth/files/shared/lib/email/email-templates.ts +0 -89
- package/templates/express/eslint.config.cjs +0 -42
- package/templates/express/src/config/helmet.ts +0 -5
- package/templates/express/src/modules/health/health.service.ts +0 -6
- package/templates/express/src/shared/errors/error-codes.ts +0 -9
- package/templates/express/src/shared/logger/logger.ts +0 -20
- package/templates/express/src/shared/utils/async-handler.ts +0 -9
- package/templates/express/src/shared/utils/response.ts +0 -9
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import status from "http-status";
|
|
2
|
+
import { JwtPayload } from "jsonwebtoken";
|
|
3
|
+
import { envVars } from "../../config/env";
|
|
4
|
+
import { prisma } from "../../database/prisma";
|
|
5
|
+
import { auth } from "../../lib/auth";
|
|
6
|
+
import { AppError } from "../../shared/errors/app-error";
|
|
7
|
+
import { jwtUtils } from "../../shared/utils/jwt";
|
|
8
|
+
import { tokenUtils } from "../../shared/utils/token";
|
|
9
|
+
import {
|
|
10
|
+
IChangePasswordPayload,
|
|
11
|
+
ILoginUserPayload,
|
|
12
|
+
IRegisterUserPayload,
|
|
13
|
+
IRequestUser,
|
|
14
|
+
} from "./auth.interface";
|
|
15
|
+
|
|
16
|
+
const registerUser = async (payload: IRegisterUserPayload) => {
|
|
17
|
+
const { name, email, password } = payload;
|
|
18
|
+
|
|
19
|
+
const data = await auth.api.signUpEmail({
|
|
20
|
+
body: {
|
|
21
|
+
name,
|
|
22
|
+
email,
|
|
23
|
+
password,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!data.user) {
|
|
28
|
+
throw new AppError(status.BAD_REQUEST, "Failed to register user");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const accessToken = tokenUtils.getAccessToken({
|
|
33
|
+
userId: data.user.id,
|
|
34
|
+
role: data.user.role,
|
|
35
|
+
name: data.user.name,
|
|
36
|
+
email: data.user.email,
|
|
37
|
+
status: data.user.status,
|
|
38
|
+
isDeleted: data.user.isDeleted,
|
|
39
|
+
emailVerified: data.user.emailVerified,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const refreshToken = tokenUtils.getRefreshToken({
|
|
43
|
+
userId: data.user.id,
|
|
44
|
+
role: data.user.role,
|
|
45
|
+
name: data.user.name,
|
|
46
|
+
email: data.user.email,
|
|
47
|
+
status: data.user.status,
|
|
48
|
+
isDeleted: data.user.isDeleted,
|
|
49
|
+
emailVerified: data.user.emailVerified,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...data,
|
|
54
|
+
accessToken,
|
|
55
|
+
refreshToken,
|
|
56
|
+
user: data.user,
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await prisma.user.delete({
|
|
60
|
+
where: {
|
|
61
|
+
id: data.user.id,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const loginUser = async (payload: ILoginUserPayload) => {
|
|
70
|
+
const { email, password } = payload;
|
|
71
|
+
|
|
72
|
+
const data = await auth.api.signInEmail({
|
|
73
|
+
body: {
|
|
74
|
+
email,
|
|
75
|
+
password,
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (data.user.status === "BLOCKED") {
|
|
80
|
+
throw new AppError(status.FORBIDDEN, "User is blocked");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (data.user.isDeleted || data.user.status === "DELETED") {
|
|
84
|
+
throw new AppError(status.NOT_FOUND, "User is deleted");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const accessToken = tokenUtils.getAccessToken({
|
|
88
|
+
userId: data.user.id,
|
|
89
|
+
role: data.user.role,
|
|
90
|
+
name: data.user.name,
|
|
91
|
+
email: data.user.email,
|
|
92
|
+
status: data.user.status,
|
|
93
|
+
isDeleted: data.user.isDeleted,
|
|
94
|
+
emailVerified: data.user.emailVerified,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const refreshToken = tokenUtils.getRefreshToken({
|
|
98
|
+
userId: data.user.id,
|
|
99
|
+
role: data.user.role,
|
|
100
|
+
name: data.user.name,
|
|
101
|
+
email: data.user.email,
|
|
102
|
+
status: data.user.status,
|
|
103
|
+
isDeleted: data.user.isDeleted,
|
|
104
|
+
emailVerified: data.user.emailVerified,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...data,
|
|
109
|
+
accessToken,
|
|
110
|
+
refreshToken,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const getMe = async (user : IRequestUser) => {
|
|
115
|
+
const isUserExists = await prisma.user.findUnique({
|
|
116
|
+
where: {
|
|
117
|
+
id: user.id,
|
|
118
|
+
},
|
|
119
|
+
// Include other related models if needed
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!isUserExists) {
|
|
123
|
+
throw new AppError(status.NOT_FOUND, "User not found");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return isUserExists;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const getNewToken = async (refreshToken : string, sessionToken : string) => {
|
|
130
|
+
|
|
131
|
+
const isSessionTokenExists = await prisma.session.findUnique({
|
|
132
|
+
where : {
|
|
133
|
+
token : sessionToken,
|
|
134
|
+
},
|
|
135
|
+
include : {
|
|
136
|
+
user : true,
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if(!isSessionTokenExists){
|
|
141
|
+
throw new AppError(status.UNAUTHORIZED, "Invalid session token");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const verifiedRefreshToken = jwtUtils.verifyToken(refreshToken, envVars.REFRESH_TOKEN_SECRET)
|
|
145
|
+
|
|
146
|
+
if(!verifiedRefreshToken.success && verifiedRefreshToken.error){
|
|
147
|
+
throw new AppError(status.UNAUTHORIZED, "Invalid refresh token");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const data = verifiedRefreshToken.data as JwtPayload;
|
|
151
|
+
|
|
152
|
+
const newAccessToken = tokenUtils.getAccessToken({
|
|
153
|
+
userId: data.userId,
|
|
154
|
+
role: data.role,
|
|
155
|
+
name: data.name,
|
|
156
|
+
email: data.email,
|
|
157
|
+
status: data.status,
|
|
158
|
+
isDeleted: data.isDeleted,
|
|
159
|
+
emailVerified: data.emailVerified,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const newRefreshToken = tokenUtils.getRefreshToken({
|
|
163
|
+
userId: data.userId,
|
|
164
|
+
role: data.role,
|
|
165
|
+
name: data.name,
|
|
166
|
+
email: data.email,
|
|
167
|
+
status: data.status,
|
|
168
|
+
isDeleted: data.isDeleted,
|
|
169
|
+
emailVerified: data.emailVerified,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const {token} = await prisma.session.update({
|
|
173
|
+
where : {
|
|
174
|
+
token : sessionToken
|
|
175
|
+
},
|
|
176
|
+
data : {
|
|
177
|
+
token : sessionToken,
|
|
178
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 60 * 24 * 1000),
|
|
179
|
+
updatedAt: new Date(),
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
accessToken : newAccessToken,
|
|
185
|
+
refreshToken : newRefreshToken,
|
|
186
|
+
sessionToken : token,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const changePassword = async (payload : IChangePasswordPayload, sessionToken : string) =>{
|
|
192
|
+
const session = await auth.api.getSession({
|
|
193
|
+
headers : new Headers({
|
|
194
|
+
Authorization : `Bearer ${sessionToken}`
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if(!session){
|
|
199
|
+
throw new AppError(status.UNAUTHORIZED, "Invalid session token");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const {currentPassword, newPassword} = payload;
|
|
203
|
+
|
|
204
|
+
const result = await auth.api.changePassword({
|
|
205
|
+
body :{
|
|
206
|
+
currentPassword,
|
|
207
|
+
newPassword,
|
|
208
|
+
revokeOtherSessions: true,
|
|
209
|
+
},
|
|
210
|
+
headers : new Headers({
|
|
211
|
+
Authorization : `Bearer ${sessionToken}`
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if(session.user.needPasswordChange){
|
|
216
|
+
await prisma.user.update({
|
|
217
|
+
where: {
|
|
218
|
+
id: session.user.id,
|
|
219
|
+
},
|
|
220
|
+
data: {
|
|
221
|
+
needPasswordChange: false,
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const accessToken = tokenUtils.getAccessToken({
|
|
227
|
+
userId: session.user.id,
|
|
228
|
+
role: session.user.role,
|
|
229
|
+
name: session.user.name,
|
|
230
|
+
email: session.user.email,
|
|
231
|
+
status: session.user.status,
|
|
232
|
+
isDeleted: session.user.isDeleted,
|
|
233
|
+
emailVerified: session.user.emailVerified,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const refreshToken = tokenUtils.getRefreshToken({
|
|
237
|
+
userId: session.user.id,
|
|
238
|
+
role: session.user.role,
|
|
239
|
+
name: session.user.name,
|
|
240
|
+
email: session.user.email,
|
|
241
|
+
status: session.user.status,
|
|
242
|
+
isDeleted: session.user.isDeleted,
|
|
243
|
+
emailVerified: session.user.emailVerified,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...result,
|
|
249
|
+
accessToken,
|
|
250
|
+
refreshToken,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const logoutUser = async (sessionToken : string) => {
|
|
255
|
+
const result = await auth.api.signOut({
|
|
256
|
+
headers : new Headers({
|
|
257
|
+
Authorization : `Bearer ${sessionToken}`
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const verifyEmail = async (email : string, otp : string) => {
|
|
265
|
+
|
|
266
|
+
const result = await auth.api.verifyEmailOTP({
|
|
267
|
+
body:{
|
|
268
|
+
email,
|
|
269
|
+
otp,
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
if(result.status && !result.user.emailVerified){
|
|
274
|
+
await prisma.user.update({
|
|
275
|
+
where : {
|
|
276
|
+
email,
|
|
277
|
+
},
|
|
278
|
+
data : {
|
|
279
|
+
emailVerified: true,
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const forgetPassword = async (email : string) => {
|
|
286
|
+
const isUserExist = await prisma.user.findUnique({
|
|
287
|
+
where : {
|
|
288
|
+
email,
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
if(!isUserExist){
|
|
293
|
+
throw new AppError(status.NOT_FOUND, "User not found");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if(!isUserExist.emailVerified){
|
|
297
|
+
throw new AppError(status.BAD_REQUEST, "Email not verified");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (isUserExist.isDeleted || isUserExist.status === "DELETED") {
|
|
301
|
+
throw new AppError(status.NOT_FOUND, "User not found");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await auth.api.requestPasswordResetEmailOTP({
|
|
305
|
+
body:{
|
|
306
|
+
email,
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const resetPassword = async (email : string, otp : string, newPassword : string) => {
|
|
312
|
+
const isUserExist = await prisma.user.findUnique({
|
|
313
|
+
where: {
|
|
314
|
+
email,
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
if (!isUserExist) {
|
|
319
|
+
throw new AppError(status.NOT_FOUND, "User not found");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!isUserExist.emailVerified) {
|
|
323
|
+
throw new AppError(status.BAD_REQUEST, "Email not verified");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isUserExist.isDeleted || isUserExist.status === "DELETED") {
|
|
327
|
+
throw new AppError(status.NOT_FOUND, "User not found");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await auth.api.resetPasswordEmailOTP({
|
|
331
|
+
body:{
|
|
332
|
+
email,
|
|
333
|
+
otp,
|
|
334
|
+
password : newPassword,
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
if (isUserExist.needPasswordChange) {
|
|
339
|
+
await prisma.user.update({
|
|
340
|
+
where: {
|
|
341
|
+
id: isUserExist.id,
|
|
342
|
+
},
|
|
343
|
+
data: {
|
|
344
|
+
needPasswordChange: false,
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await prisma.session.deleteMany({
|
|
350
|
+
where:{
|
|
351
|
+
userId : isUserExist.id,
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
|
+
const googleLoginSuccess = async (session : Record<string, any>) =>{
|
|
358
|
+
const isCustomerExists = await prisma.user.findUnique({
|
|
359
|
+
where: {
|
|
360
|
+
id: session.user.id,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (!isCustomerExists) {
|
|
365
|
+
await prisma.user.create({
|
|
366
|
+
data: {
|
|
367
|
+
id: session.user.id,
|
|
368
|
+
name: session.user.name,
|
|
369
|
+
email: session.user.email,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const accessToken = tokenUtils.getAccessToken({
|
|
375
|
+
userId: session.user.id,
|
|
376
|
+
role: session.user.role,
|
|
377
|
+
name: session.user.name,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const refreshToken = tokenUtils.getRefreshToken({
|
|
381
|
+
userId: session.user.id,
|
|
382
|
+
role: session.user.role,
|
|
383
|
+
name: session.user.name,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
accessToken,
|
|
388
|
+
refreshToken,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export const authService = {
|
|
393
|
+
registerUser: registerUser,
|
|
394
|
+
loginUser,
|
|
395
|
+
getMe,
|
|
396
|
+
getNewToken,
|
|
397
|
+
changePassword,
|
|
398
|
+
logoutUser,
|
|
399
|
+
verifyEmail,
|
|
400
|
+
forgetPassword,
|
|
401
|
+
resetPassword,
|
|
402
|
+
googleLoginSuccess,
|
|
403
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
8
|
+
<title>
|
|
9
|
+
<%= appName || "Your App" %> - Continue with Google
|
|
10
|
+
</title>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body style="margin:0; padding:0; background-color:#f3f4f6;">
|
|
14
|
+
<div style="display:none; max-height:0; overflow:hidden; opacity:0; color:transparent;">
|
|
15
|
+
Continue your Google sign-in securely.
|
|
16
|
+
</div>
|
|
17
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
|
|
18
|
+
style="background-color:#f3f4f6; padding:24px 12px;">
|
|
19
|
+
<tr>
|
|
20
|
+
<td align="center">
|
|
21
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
|
|
22
|
+
style="max-width:600px; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
|
|
23
|
+
<tr>
|
|
24
|
+
<td style="padding:28px 24px 16px 24px; font-family:Arial, Helvetica, sans-serif; color:#111827;">
|
|
25
|
+
<h1 style="margin:0; font-size:22px; line-height:30px; font-weight:700;">Continue with
|
|
26
|
+
Google</h1>
|
|
27
|
+
<p style="margin:14px 0 0; font-size:15px; line-height:24px; color:#374151;">
|
|
28
|
+
Hi <%= userName || "there" %>,
|
|
29
|
+
</p>
|
|
30
|
+
<p style="margin:10px 0 0; font-size:15px; line-height:24px; color:#374151;">
|
|
31
|
+
We received a request to continue authentication with Google for your <strong>
|
|
32
|
+
<%= appName || "account" %>
|
|
33
|
+
</strong>.
|
|
34
|
+
</p>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
|
|
38
|
+
<tr>
|
|
39
|
+
<td style="padding:0 24px 24px 24px; font-family:Arial, Helvetica, sans-serif;">
|
|
40
|
+
<table role="presentation" cellspacing="0" cellpadding="0" border="0">
|
|
41
|
+
<tr>
|
|
42
|
+
<td align="center" bgcolor="#111827" style="border-radius:8px;">
|
|
43
|
+
<a href="<%= redirectUrl %>" target="_blank"
|
|
44
|
+
style="display:inline-block; padding:12px 22px; font-size:15px; line-height:20px; font-weight:600; color:#ffffff; text-decoration:none;">
|
|
45
|
+
Continue Sign-in
|
|
46
|
+
</a>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
</table>
|
|
50
|
+
|
|
51
|
+
<p
|
|
52
|
+
style="margin:14px 0 0; font-size:13px; line-height:20px; color:#6b7280; word-break:break-word;">
|
|
53
|
+
Button not working? Copy and paste this URL into your browser:<br />
|
|
54
|
+
<a href="<%= redirectUrl %>" target="_blank"
|
|
55
|
+
style="color:#2563eb; text-decoration:underline;">
|
|
56
|
+
<%= redirectUrl %>
|
|
57
|
+
</a>
|
|
58
|
+
</p>
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
|
|
62
|
+
<tr>
|
|
63
|
+
<td style="padding:0 24px 24px 24px; font-family:Arial, Helvetica, sans-serif; color:#4b5563;">
|
|
64
|
+
<p style="margin:0; font-size:14px; line-height:22px;">
|
|
65
|
+
If you didn’t request this, you can safely ignore this email. No changes will be made
|
|
66
|
+
unless you continue.
|
|
67
|
+
</p>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
|
|
71
|
+
<tr>
|
|
72
|
+
<td
|
|
73
|
+
style="padding:16px 24px 24px 24px; border-top:1px solid #e5e7eb; font-family:Arial, Helvetica, sans-serif; color:#6b7280;">
|
|
74
|
+
<p style="margin:0; font-size:12px; line-height:18px;">
|
|
75
|
+
Need help? Contact us at
|
|
76
|
+
<a href="mailto:<%= supportEmail || " support@example.com" %>" style="color:#2563eb;
|
|
77
|
+
text-decoration:underline;"><%= supportEmail || "support@example.com" %></a>.
|
|
78
|
+
</p>
|
|
79
|
+
<p style="margin:6px 0 0; font-size:12px; line-height:18px;">
|
|
80
|
+
© <%= year || new Date().getFullYear() %>
|
|
81
|
+
<%= appName || "Your App" %>. All rights reserved.
|
|
82
|
+
</p>
|
|
83
|
+
</td>
|
|
84
|
+
</tr>
|
|
85
|
+
</table>
|
|
86
|
+
</td>
|
|
87
|
+
</tr>
|
|
88
|
+
</table>
|
|
89
|
+
</body>
|
|
90
|
+
|
|
91
|
+
</html>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
8
|
+
<title>
|
|
9
|
+
<%= appName || "Your App" %> - Your OTP Code
|
|
10
|
+
</title>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body style="margin:0; padding:0; background-color:#f3f4f6;">
|
|
14
|
+
<div style="display:none; max-height:0; overflow:hidden; opacity:0; color:transparent;">
|
|
15
|
+
Your one-time verification code is ready.
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
|
|
19
|
+
style="background-color:#f3f4f6; padding:24px 12px;">
|
|
20
|
+
<tr>
|
|
21
|
+
<td align="center">
|
|
22
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"
|
|
23
|
+
style="max-width:600px; background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; overflow:hidden;">
|
|
24
|
+
<tr>
|
|
25
|
+
<td
|
|
26
|
+
style="padding:28px 24px 16px 24px; font-family:Arial, Helvetica, sans-serif; color:#111827;">
|
|
27
|
+
<h1 style="margin:0; font-size:22px; line-height:30px; font-weight:700;">Verification Code
|
|
28
|
+
</h1>
|
|
29
|
+
<p style="margin:14px 0 0; font-size:15px; line-height:24px; color:#374151;">
|
|
30
|
+
Hi <%= userName || "there" %>,
|
|
31
|
+
</p>
|
|
32
|
+
<p style="margin:10px 0 0; font-size:15px; line-height:24px; color:#374151;">
|
|
33
|
+
Use the one-time password (OTP) below to verify your identity for <strong>
|
|
34
|
+
<%= appName || "your account" %>
|
|
35
|
+
</strong>.
|
|
36
|
+
</p>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
|
|
40
|
+
<tr>
|
|
41
|
+
<td align="center"
|
|
42
|
+
style="padding:8px 24px 20px 24px; font-family:Arial, Helvetica, sans-serif;">
|
|
43
|
+
<div
|
|
44
|
+
style="display:inline-block; padding:12px 20px; border-radius:10px; border:1px solid #d1d5db; background:#f9fafb; letter-spacing:7px; font-size:28px; line-height:34px; font-weight:700; color:#111827;">
|
|
45
|
+
<%= otp %>
|
|
46
|
+
</div>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
|
|
50
|
+
<tr>
|
|
51
|
+
<td style="padding:0 24px 22px 24px; font-family:Arial, Helvetica, sans-serif; color:#4b5563;">
|
|
52
|
+
<p style="margin:0; font-size:14px; line-height:22px;">
|
|
53
|
+
This code will expire in <strong>
|
|
54
|
+
<%= expiresInMinutes || 2 %> minutes
|
|
55
|
+
</strong>.
|
|
56
|
+
</p>
|
|
57
|
+
<p style="margin:10px 0 0; font-size:14px; line-height:22px;">
|
|
58
|
+
If you didn’t request this code, please ignore this email and consider changing your
|
|
59
|
+
password.
|
|
60
|
+
</p>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
|
|
64
|
+
<tr>
|
|
65
|
+
<td
|
|
66
|
+
style="padding:16px 24px 24px 24px; border-top:1px solid #e5e7eb; font-family:Arial, Helvetica, sans-serif; color:#6b7280;">
|
|
67
|
+
<p style="margin:0; font-size:12px; line-height:18px;">
|
|
68
|
+
For security, never share this OTP with anyone.
|
|
69
|
+
</p>
|
|
70
|
+
<p style="margin:6px 0 0; font-size:12px; line-height:18px;">
|
|
71
|
+
Need help? Contact
|
|
72
|
+
<a href="mailto:<%= supportEmail || " support@example.com" %>" style="color:#2563eb;
|
|
73
|
+
text-decoration:underline;"><%= supportEmail || "support@example.com" %></a>.
|
|
74
|
+
</p>
|
|
75
|
+
<p style="margin:6px 0 0; font-size:12px; line-height:18px;">
|
|
76
|
+
© <%= year || new Date().getFullYear() %>
|
|
77
|
+
<%= appName || "Your App" %>. All rights reserved.
|
|
78
|
+
</p>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
</table>
|
|
82
|
+
</td>
|
|
83
|
+
</tr>
|
|
84
|
+
</table>
|
|
85
|
+
</body>
|
|
86
|
+
|
|
87
|
+
</html>
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
declare global {
|
|
2
2
|
namespace Express {
|
|
3
3
|
interface Request {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
4
|
+
user: {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
role: string;
|
|
9
|
+
};
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export { };
|
|
16
|
-
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CookieOptions, Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
const setCookie = (res: Response, key: string, value: string, options: CookieOptions) => {
|
|
4
|
+
res.cookie(key, value, options);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const getCookie = (req: Request, key: string) => {
|
|
8
|
+
return req.cookies[key];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const clearCookie = (res: Response, key: string, options: CookieOptions) => {
|
|
12
|
+
res.clearCookie(key, options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const cookieUtils = {
|
|
16
|
+
setCookie,
|
|
17
|
+
getCookie,
|
|
18
|
+
clearCookie,
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import jwt, { JwtPayload, SignOptions } from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
const createToken = (payload: JwtPayload, secret: string, { expiresIn }: SignOptions) => {
|
|
4
|
+
const token = jwt.sign(payload, secret, { expiresIn });
|
|
5
|
+
return token;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const verifyToken = (token: string, secret: string) => {
|
|
9
|
+
try {
|
|
10
|
+
const decoded = jwt.verify(token, secret) as JwtPayload;
|
|
11
|
+
return {
|
|
12
|
+
success: true,
|
|
13
|
+
data: decoded
|
|
14
|
+
}
|
|
15
|
+
} catch (error: unknown) {
|
|
16
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
message,
|
|
20
|
+
error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const decodeToken = (token: string) => {
|
|
26
|
+
const decoded = jwt.decode(token) as JwtPayload;
|
|
27
|
+
return decoded;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const jwtUtils = {
|
|
31
|
+
createToken,
|
|
32
|
+
verifyToken,
|
|
33
|
+
decodeToken,
|
|
34
|
+
}
|