exp-node-server 1.1.7 → 2.0.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/README.md +82 -20
- package/dist/cli.js +525 -0
- package/package.json +29 -9
- package/templates/mongoose/.dockerignore +10 -0
- package/templates/mongoose/.env.example +56 -7
- package/templates/mongoose/.prettierignore +3 -0
- package/templates/mongoose/.prettierrc +7 -0
- package/templates/mongoose/Dockerfile +33 -0
- package/templates/mongoose/_gitignore +32 -0
- package/templates/mongoose/docker-compose.yml +21 -0
- package/templates/mongoose/eslint.config.js +35 -0
- package/templates/mongoose/package-lock.json +6377 -0
- package/templates/mongoose/package.json +49 -10
- package/templates/mongoose/readme.md +85 -0
- package/templates/mongoose/src/app/configs/index.ts +178 -26
- package/templates/mongoose/src/app/errors/zodError.ts +16 -16
- package/templates/mongoose/src/app/lib/db.ts +18 -0
- package/templates/mongoose/src/app/lib/logger.ts +12 -0
- package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
- package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
- package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
- package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
- package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
- package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
- package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
- package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
- package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
- package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
- package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
- package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
- package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
- package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
- package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
- package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
- package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
- package/templates/mongoose/src/app/passport-js/index.ts +87 -0
- package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
- package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
- package/templates/mongoose/src/app/queues/agenda.ts +24 -0
- package/templates/mongoose/src/app/queues/email.queue.ts +53 -0
- package/templates/mongoose/src/app/queues/index.ts +13 -0
- package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/mongoose/src/app/types/error.d.ts +7 -7
- package/templates/mongoose/src/app/types/express.d.ts +11 -0
- package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/mongoose/src/app/utils/JWT.ts +38 -18
- package/templates/mongoose/src/app/utils/app_error.ts +10 -10
- package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
- package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
- package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
- package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
- package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
- package/templates/mongoose/src/app/utils/otp.ts +20 -0
- package/templates/mongoose/src/app/utils/session.ts +55 -0
- package/templates/mongoose/src/app.ts +73 -42
- package/templates/mongoose/src/routes.ts +13 -13
- package/templates/mongoose/src/server.ts +38 -18
- package/templates/mongoose/src/swaggerOptions.ts +23 -23
- package/templates/mongoose/tests/app.test.ts +97 -0
- package/templates/mongoose/tests/global-setup.ts +15 -0
- package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
- package/templates/mongoose/tests/routes/account.test.ts +614 -0
- package/templates/mongoose/tests/routes/profile.test.ts +95 -0
- package/templates/mongoose/tests/routes/social.test.ts +310 -0
- package/templates/mongoose/tests/setup.ts +25 -0
- package/templates/mongoose/tsconfig.json +16 -20
- package/templates/mongoose/vitest.config.ts +43 -0
- package/templates/prisma/.dockerignore +2 -0
- package/templates/prisma/.env.example +62 -6
- package/templates/prisma/.prettierignore +3 -0
- package/templates/prisma/.prettierrc +7 -0
- package/templates/prisma/.projectrc.json +1 -1
- package/templates/prisma/Dockerfile +21 -7
- package/templates/prisma/_gitignore +35 -0
- package/templates/prisma/docker-compose.yml +8 -7
- package/templates/prisma/eslint.config.js +35 -0
- package/templates/prisma/package-lock.json +7327 -0
- package/templates/prisma/package.json +47 -9
- package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
- package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
- package/templates/prisma/prisma/schema/schema.prisma +1 -2
- package/templates/prisma/prisma.config.ts +5 -5
- package/templates/prisma/readme.md +81 -0
- package/templates/prisma/src/app/configs/index.ts +175 -26
- package/templates/prisma/src/app/errors/zodError.ts +16 -16
- package/templates/prisma/src/app/lib/logger.ts +12 -0
- package/templates/prisma/src/app/lib/prisma.ts +4 -6
- package/templates/prisma/src/app/middlewares/auth.ts +37 -16
- package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
- package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
- package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
- package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
- package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
- package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
- package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
- package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
- package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
- package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
- package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
- package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
- package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
- package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
- package/templates/prisma/src/app/passport-js/index.ts +87 -0
- package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/prisma/src/app/passport-js/providers.ts +263 -0
- package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
- package/templates/prisma/src/app/queues/boss.ts +8 -0
- package/templates/prisma/src/app/queues/email.queue.ts +56 -0
- package/templates/prisma/src/app/queues/index.ts +14 -0
- package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
- package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/prisma/src/app/types/error.d.ts +7 -7
- package/templates/prisma/src/app/types/express.d.ts +11 -0
- package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/prisma/src/app/utils/JWT.ts +36 -17
- package/templates/prisma/src/app/utils/app_error.ts +10 -10
- package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
- package/templates/prisma/src/app/utils/catch_async.ts +8 -8
- package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
- package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
- package/templates/prisma/src/app/utils/manage_response.ts +19 -19
- package/templates/prisma/src/app/utils/otp.ts +20 -0
- package/templates/prisma/src/app/utils/session.ts +57 -0
- package/templates/prisma/src/app.ts +73 -42
- package/templates/prisma/src/routes.ts +12 -10
- package/templates/prisma/src/server.ts +37 -19
- package/templates/prisma/src/swaggerOptions.ts +22 -26
- package/templates/prisma/tests/app.test.ts +86 -0
- package/templates/prisma/tests/helpers/fixtures.ts +35 -0
- package/templates/prisma/tests/helpers/prisma.ts +32 -0
- package/templates/prisma/tests/routes/account.test.ts +627 -0
- package/templates/prisma/tests/routes/profile.test.ts +91 -0
- package/templates/prisma/tests/routes/social.test.ts +317 -0
- package/templates/prisma/tests/setup.ts +10 -0
- package/templates/prisma/tsconfig.json +16 -23
- package/templates/prisma/vitest.config.ts +38 -0
- package/bin/cli.js +0 -41
- package/src/createProject.js +0 -61
- package/src/generateModule.js +0 -93
- package/src/utils/getProjectType.js +0 -13
- package/src/utils/mongoose/controllerGenerator.js +0 -70
- package/src/utils/mongoose/routeGenerator.js +0 -27
- package/src/utils/mongoose/schemaGenerator.js +0 -11
- package/src/utils/mongoose/serviceGenerator.js +0 -49
- package/src/utils/mongoose/swaggerGenerator.js +0 -112
- package/src/utils/mongoose/validationGenerator.js +0 -13
- package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
- package/src/utils/prisma/prismaServiceGenerator.js +0 -47
- package/src/utils/routeInjector.js +0 -40
- package/src/utils/swaggerInjector.js +0 -52
- package/src/utils/validateModuleName.js +0 -17
- package/src/utils/validateProjectName.js +0 -15
- package/src/utils.js +0 -26
- package/templates/mongoose/src/app/errors/castError.ts +0 -23
- package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
- package/templates/mongoose/src/app/errors/validationError.ts +0 -25
- package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
- package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
- package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
- package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
- package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
- package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
- package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
- package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
- package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
- package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
- package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
- package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
- package/templates/mongoose/src/app/types/index.d.ts +0 -12
- package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
- package/templates/prisma/src/app/types/index.d.ts +0 -12
- package/templates/prisma/src/app/utils/otpGenerator.ts +0 -29
|
@@ -1,394 +1,410 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
1
|
+
import { Prisma } from '@prisma/client';
|
|
2
|
+
import bcrypt from 'bcrypt';
|
|
3
|
+
import type { Request } from 'express';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { configs } from '../../configs/index.js';
|
|
6
|
+
import { prisma } from '../../lib/prisma.js';
|
|
7
|
+
import { hashLoginCode } from '../../passport-js/social-account.js';
|
|
8
|
+
import { enqueueEmail } from '../../queues/email.queue.js';
|
|
9
|
+
import { AppError } from '../../utils/app_error.js';
|
|
10
|
+
import { jwtHelpers, type TokenUser } from '../../utils/JWT.js';
|
|
11
|
+
import { hashRefreshToken, readRefreshToken } from '../../utils/session.js';
|
|
12
|
+
import {
|
|
13
|
+
generateOtp,
|
|
14
|
+
hashOtp,
|
|
15
|
+
isOtpMatch,
|
|
16
|
+
OTP_EXPIRY_MS,
|
|
17
|
+
OTP_MAX_ATTEMPTS,
|
|
18
|
+
OTP_RESEND_COOLDOWN_MS,
|
|
19
|
+
} from '../../utils/otp.js';
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
// cheap hashes under tests, the real cost everywhere else
|
|
22
|
+
const BCRYPT_ROUNDS = configs.is_test ? 4 : 12;
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
// a rotated refresh token that comes back within this window is most likely a
|
|
25
|
+
// parallel refresh (two tabs at once), not theft, so it doesn't end the session
|
|
26
|
+
const REFRESH_REUSE_GRACE_MS = 30 * 1000;
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
data: {
|
|
20
|
-
email: payload.email,
|
|
21
|
-
password: hashPassword,
|
|
22
|
-
},
|
|
23
|
-
});
|
|
28
|
+
// a valid hash to compare against when the account doesn't exist, so login takes
|
|
29
|
+
// the same time either way (prevents finding registered emails by timing)
|
|
30
|
+
const DUMMY_HASH = bcrypt.hashSync('dummy-password', BCRYPT_ROUNDS);
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
accountId: account.id,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
32
|
+
type VerifyTokenPayload = { purpose: 'verify-account'; accountId: string };
|
|
33
|
+
// `tv` is the account's token version; any password change makes the link stale
|
|
34
|
+
type ResetTokenPayload = { purpose: 'reset-password'; accountId: string; tv: number };
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const toTokenUser = (account: {
|
|
37
|
+
id: string;
|
|
38
|
+
email: string;
|
|
39
|
+
role: TokenUser['role'];
|
|
40
|
+
tokenVersion: number;
|
|
41
|
+
}): TokenUser => ({
|
|
42
|
+
accountId: account.id,
|
|
43
|
+
email: account.email,
|
|
44
|
+
role: account.role,
|
|
45
|
+
tokenVersion: account.tokenVersion,
|
|
46
|
+
});
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
configs.jwt.verified_token as string,
|
|
46
|
-
"5m",
|
|
48
|
+
/** A fresh OTP + verification link, and the columns to store for it. */
|
|
49
|
+
const newVerification = (accountId: string) => {
|
|
50
|
+
const otp = generateOtp();
|
|
51
|
+
const token = jwtHelpers.generateToken(
|
|
52
|
+
{ purpose: 'verify-account', accountId } satisfies VerifyTokenPayload,
|
|
53
|
+
configs.jwt.verified_token,
|
|
54
|
+
'5m',
|
|
47
55
|
);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
data: {
|
|
55
|
-
lastOtp: newOtp,
|
|
56
|
-
lastOtpSendingTime: new Date(),
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await sendMail({
|
|
61
|
-
to: payload.email as string,
|
|
62
|
-
subject: "welcome to - Please verify your account",
|
|
63
|
-
htmlBody: `
|
|
64
|
-
<p><strong>OTP</strong> ${newOtp}</p>
|
|
65
|
-
<small>Otp will be expire in 5 minutes</small>
|
|
56
|
+
return {
|
|
57
|
+
otp,
|
|
58
|
+
verificationLink: `${configs.jwt.front_end_url}/verify?token=${encodeURIComponent(token)}`,
|
|
59
|
+
data: { lastOtp: hashOtp(otp), lastOtpSendingTime: new Date(), otpAttempts: 0 },
|
|
60
|
+
};
|
|
61
|
+
};
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
const create_account_into_db = async (req: Request) => {
|
|
64
|
+
const payload: { email: string; password: string; fullName: string } = req.body;
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
textBody: "You can use otp or direct link",
|
|
73
|
-
name: payload.fullName,
|
|
66
|
+
const exists = await prisma.account.findUnique({
|
|
67
|
+
where: { email: payload.email },
|
|
68
|
+
select: { id: true },
|
|
74
69
|
});
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
if (exists) {
|
|
71
|
+
throw new AppError('An account with this email already exists', 409);
|
|
72
|
+
}
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
// id generated up front so the account and its OTP are written in one query
|
|
75
|
+
const id = randomUUID();
|
|
76
|
+
const { otp, verificationLink, data } = newVerification(id);
|
|
77
|
+
|
|
78
|
+
const account = await prisma.account.create({
|
|
79
|
+
data: {
|
|
80
|
+
id,
|
|
83
81
|
email: payload.email,
|
|
82
|
+
password: await bcrypt.hash(payload.password, BCRYPT_ROUNDS),
|
|
83
|
+
...data,
|
|
84
|
+
profile: { create: { fullName: payload.fullName } },
|
|
84
85
|
},
|
|
86
|
+
select: { id: true, email: true, role: true, isAccountVerified: true, profile: true },
|
|
85
87
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
await enqueueEmail({ type: 'verify-account', email: account.email, otp, verificationLink });
|
|
90
|
+
return account;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const verify_account_using_otp_into_db = async (req: Request) => {
|
|
94
|
+
const payload: { email: string; otp: string } = req.body;
|
|
95
|
+
const invalid = new AppError('Invalid or expired OTP', 401);
|
|
96
|
+
|
|
97
|
+
const account = await prisma.account.findUnique({ where: { email: payload.email } });
|
|
98
|
+
if (!account || account.isAccountVerified || !account.lastOtp) {
|
|
99
|
+
throw invalid;
|
|
89
100
|
}
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
|
|
102
|
+
const isExpired =
|
|
103
|
+
!account.lastOtpSendingTime ||
|
|
104
|
+
Date.now() - account.lastOtpSendingTime.getTime() > OTP_EXPIRY_MS;
|
|
105
|
+
if (isExpired) {
|
|
106
|
+
throw invalid;
|
|
94
107
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
throw new AppError("OTP Expired, Please try again!!", 401);
|
|
108
|
+
|
|
109
|
+
// use up an attempt atomically, so parallel guesses can't go past the limit
|
|
110
|
+
const attempt = await prisma.account.updateMany({
|
|
111
|
+
where: { id: account.id, otpAttempts: { lt: OTP_MAX_ATTEMPTS } },
|
|
112
|
+
data: { otpAttempts: { increment: 1 } },
|
|
113
|
+
});
|
|
114
|
+
if (!attempt.count) {
|
|
115
|
+
throw new AppError('Too many wrong attempts, please request a new OTP', 429);
|
|
104
116
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
},
|
|
117
|
+
|
|
118
|
+
if (!isOtpMatch(payload.otp, account.lastOtp)) {
|
|
119
|
+
throw invalid;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// only succeeds if this OTP wasn't replaced or used in the meantime
|
|
123
|
+
const verified = await prisma.account.updateMany({
|
|
124
|
+
where: { id: account.id, lastOtp: account.lastOtp },
|
|
125
|
+
data: { isAccountVerified: true, lastOtp: null, otpAttempts: 0 },
|
|
113
126
|
});
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
if (!verified.count) {
|
|
128
|
+
throw invalid;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
116
131
|
};
|
|
117
132
|
|
|
118
133
|
const verify_account_using_link_into_db = async (req: Request) => {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
} catch (error: any) {
|
|
127
|
-
if (error?.message == "invalid signature") {
|
|
128
|
-
throw new AppError("Invalid Token", 403);
|
|
129
|
-
} else if (error?.message == "jwt expired") {
|
|
130
|
-
throw new AppError("Token expired, please reset again", 403);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// check account
|
|
134
|
-
const account = await prisma.account.findUnique({
|
|
135
|
-
where: {
|
|
136
|
-
email: decoadeToken.email,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
// check if account exists
|
|
140
|
-
if (!account) {
|
|
141
|
-
throw new AppError("Account not found", 404);
|
|
134
|
+
const decoded = jwtHelpers.verifyToken<VerifyTokenPayload>(
|
|
135
|
+
req.body.token,
|
|
136
|
+
configs.jwt.verified_token,
|
|
137
|
+
);
|
|
138
|
+
if (decoded.purpose !== 'verify-account') {
|
|
139
|
+
throw new AppError('Invalid token', 401);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
id: account.id,
|
|
148
|
-
},
|
|
149
|
-
data: {
|
|
150
|
-
isAccountVerified: true,
|
|
151
|
-
},
|
|
142
|
+
const result = await prisma.account.updateMany({
|
|
143
|
+
where: { id: decoded.accountId, isDeleted: false },
|
|
144
|
+
data: { isAccountVerified: true, lastOtp: null, otpAttempts: 0 },
|
|
152
145
|
});
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
if (!result.count) {
|
|
147
|
+
throw new AppError('Invalid token', 401);
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
155
150
|
};
|
|
156
151
|
|
|
157
|
-
const login_user_into_db = async (req: Request) => {
|
|
158
|
-
const payload = req
|
|
159
|
-
const
|
|
160
|
-
where: {
|
|
161
|
-
email: payload.email,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// check if account exists
|
|
166
|
-
if (!account) {
|
|
167
|
-
throw new AppError("Account not found", 404);
|
|
168
|
-
}
|
|
152
|
+
const login_user_into_db = async (req: Request): Promise<TokenUser> => {
|
|
153
|
+
const payload: { email: string; password: string } = req.body;
|
|
154
|
+
const invalid = new AppError('Invalid email or password', 401);
|
|
169
155
|
|
|
170
|
-
|
|
171
|
-
const isPasswordMatch = bcrypt.compareSync(
|
|
172
|
-
payload.password,
|
|
173
|
-
account.password,
|
|
174
|
-
);
|
|
156
|
+
const account = await prisma.account.findUnique({ where: { email: payload.email } });
|
|
175
157
|
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
// always run bcrypt, and never skip the password check: social-login accounts
|
|
159
|
+
// have no password and must sign in through their provider
|
|
160
|
+
const isPasswordMatch = await bcrypt.compare(payload.password, account?.password ?? DUMMY_HASH);
|
|
161
|
+
if (!account || !account.password || !isPasswordMatch) {
|
|
162
|
+
throw invalid;
|
|
178
163
|
}
|
|
179
164
|
|
|
180
|
-
// check if account is deleted
|
|
181
165
|
if (account.isDeleted) {
|
|
182
|
-
throw new AppError(
|
|
166
|
+
throw new AppError('Account is deleted', 401);
|
|
183
167
|
}
|
|
184
|
-
// check if account is verified
|
|
185
168
|
if (!account.isAccountVerified) {
|
|
186
|
-
throw new AppError(
|
|
169
|
+
throw new AppError('Account is not verified', 401);
|
|
187
170
|
}
|
|
188
171
|
|
|
189
|
-
|
|
190
|
-
const accessToken = jwtHelpers.generateToken(
|
|
191
|
-
{
|
|
192
|
-
email: account.email,
|
|
193
|
-
role: account.role,
|
|
194
|
-
accountId: account.id,
|
|
195
|
-
},
|
|
196
|
-
configs.jwt.access_token as string,
|
|
197
|
-
configs.jwt.access_expires as string,
|
|
198
|
-
);
|
|
199
|
-
return accessToken;
|
|
172
|
+
return toTokenUser(account);
|
|
200
173
|
};
|
|
201
174
|
|
|
202
175
|
const get_user_account_from_db = async (req: Request) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const result = await prisma.account.findUnique({
|
|
206
|
-
where: {
|
|
207
|
-
id: user?.accountId,
|
|
208
|
-
},
|
|
176
|
+
return prisma.account.findUnique({
|
|
177
|
+
where: { id: req.user!.accountId },
|
|
209
178
|
select: {
|
|
210
179
|
id: true,
|
|
211
180
|
email: true,
|
|
212
181
|
role: true,
|
|
182
|
+
provider: true,
|
|
213
183
|
isAccountVerified: true,
|
|
214
184
|
isDeleted: true,
|
|
215
185
|
profile: true,
|
|
186
|
+
socialAccounts: { select: { provider: true, createdAt: true } },
|
|
216
187
|
},
|
|
217
188
|
});
|
|
218
|
-
return result;
|
|
219
189
|
};
|
|
220
190
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (isSamePassword) {
|
|
228
|
-
throw new AppError(
|
|
229
|
-
"Old and new password are same, Please provide deffirent password",
|
|
230
|
-
404,
|
|
231
|
-
);
|
|
191
|
+
/** Changes the password and revokes every other session. Returns the user for a new token. */
|
|
192
|
+
const change_password_into_db = async (req: Request): Promise<TokenUser> => {
|
|
193
|
+
const payload: { oldPassword: string; newPassword: string } = req.body;
|
|
194
|
+
|
|
195
|
+
if (payload.oldPassword === payload.newPassword) {
|
|
196
|
+
throw new AppError('New password must be different from the old password', 400);
|
|
232
197
|
}
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
// if account not exists
|
|
240
|
-
if (!isUserExist) {
|
|
241
|
-
throw new AppError("Account not found!!", 404);
|
|
198
|
+
|
|
199
|
+
const account = await prisma.account.findUnique({ where: { id: req.user!.accountId } });
|
|
200
|
+
if (!account) {
|
|
201
|
+
throw new AppError('Account not found', 404);
|
|
242
202
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
);
|
|
203
|
+
if (!account.password) {
|
|
204
|
+
throw new AppError('This account uses social login and has no password', 400);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isPasswordMatch = await bcrypt.compare(payload.oldPassword, account.password);
|
|
248
208
|
if (!isPasswordMatch) {
|
|
249
|
-
throw new AppError(
|
|
209
|
+
throw new AppError('Incorrect password', 401);
|
|
250
210
|
}
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
where: {
|
|
255
|
-
id: isUserExist.id,
|
|
256
|
-
},
|
|
211
|
+
|
|
212
|
+
const updated = await prisma.account.update({
|
|
213
|
+
where: { id: account.id },
|
|
257
214
|
data: {
|
|
258
|
-
password:
|
|
215
|
+
password: await bcrypt.hash(payload.newPassword, BCRYPT_ROUNDS),
|
|
216
|
+
tokenVersion: { increment: 1 },
|
|
259
217
|
},
|
|
260
218
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return "";
|
|
219
|
+
await prisma.refreshToken.deleteMany({ where: { accountId: account.id } });
|
|
220
|
+
return toTokenUser(updated);
|
|
264
221
|
};
|
|
265
222
|
|
|
266
223
|
const resend_otp_and_verification_link_from_db = async (req: Request) => {
|
|
267
|
-
const email = req
|
|
224
|
+
const email: string = req.body.email;
|
|
268
225
|
const account = await prisma.account.findUnique({
|
|
269
|
-
where: {
|
|
270
|
-
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
// check if account exists
|
|
274
|
-
if (!account) {
|
|
275
|
-
throw new AppError("Account not found", 404);
|
|
276
|
-
}
|
|
277
|
-
// if already verified
|
|
278
|
-
if (account.isAccountVerified) {
|
|
279
|
-
throw new AppError("Account already verified", 403);
|
|
280
|
-
}
|
|
281
|
-
// make new otp and verification link
|
|
282
|
-
const newOtp = otpGenerator();
|
|
283
|
-
const verificationToken = jwtHelpers.generateToken(
|
|
284
|
-
{
|
|
285
|
-
email,
|
|
286
|
-
accountId: account.id,
|
|
287
|
-
},
|
|
288
|
-
configs.jwt.verified_token as string,
|
|
289
|
-
"5m",
|
|
290
|
-
);
|
|
291
|
-
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
|
|
292
|
-
// save otp into db
|
|
293
|
-
await prisma.account.update({
|
|
294
|
-
where: {
|
|
295
|
-
email,
|
|
296
|
-
},
|
|
297
|
-
data: {
|
|
298
|
-
lastOtp: newOtp,
|
|
299
|
-
lastOtpSendingTime: new Date(),
|
|
300
|
-
},
|
|
226
|
+
where: { email },
|
|
227
|
+
select: { id: true, isAccountVerified: true },
|
|
301
228
|
});
|
|
302
229
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
subject: "New Verification otp and link",
|
|
306
|
-
htmlBody: `
|
|
307
|
-
<p><strong>OTP</strong> ${newOtp}</p>
|
|
308
|
-
<small>Otp will be expire in 5 minutes</small>
|
|
230
|
+
// same response whether or not the account exists (no email enumeration)
|
|
231
|
+
if (!account || account.isAccountVerified) return null;
|
|
309
232
|
|
|
310
|
-
|
|
233
|
+
const { otp, verificationLink, data } = newVerification(account.id);
|
|
311
234
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
235
|
+
// the cooldown check is part of the update, so parallel requests send one email
|
|
236
|
+
const result = await prisma.account.updateMany({
|
|
237
|
+
where: {
|
|
238
|
+
id: account.id,
|
|
239
|
+
isAccountVerified: false,
|
|
240
|
+
OR: [
|
|
241
|
+
{ lastOtpSendingTime: null },
|
|
242
|
+
{ lastOtpSendingTime: { lt: new Date(Date.now() - OTP_RESEND_COOLDOWN_MS) } },
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
data,
|
|
316
246
|
});
|
|
247
|
+
if (!result.count) {
|
|
248
|
+
throw new AppError('Please wait a minute before requesting a new OTP', 429);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await enqueueEmail({ type: 'verify-account', email, otp, verificationLink });
|
|
252
|
+
return null;
|
|
317
253
|
};
|
|
318
254
|
|
|
319
|
-
const
|
|
320
|
-
const email = req
|
|
255
|
+
const forget_password_generate_reset_token_from_db = async (req: Request) => {
|
|
256
|
+
const email: string = req.body.email;
|
|
321
257
|
const account = await prisma.account.findUnique({
|
|
322
|
-
where: {
|
|
323
|
-
|
|
324
|
-
},
|
|
258
|
+
where: { email },
|
|
259
|
+
select: { id: true, isDeleted: true, tokenVersion: true },
|
|
325
260
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const verificationToken = jwtHelpers.generateToken(
|
|
261
|
+
|
|
262
|
+
// same response whether or not the account exists (no email enumeration)
|
|
263
|
+
if (!account || account.isDeleted) return null;
|
|
264
|
+
|
|
265
|
+
const resetToken = jwtHelpers.generateToken(
|
|
332
266
|
{
|
|
333
|
-
|
|
267
|
+
purpose: 'reset-password',
|
|
334
268
|
accountId: account.id,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
269
|
+
tv: account.tokenVersion,
|
|
270
|
+
} satisfies ResetTokenPayload,
|
|
271
|
+
configs.jwt.reset_secret,
|
|
272
|
+
configs.jwt.reset_expires,
|
|
338
273
|
);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
await
|
|
342
|
-
|
|
343
|
-
subject: "Forget Password- Use this link for new password ",
|
|
344
|
-
htmlBody: `
|
|
345
|
-
<p>Your Reset Link: </p>
|
|
346
|
-
<p>${verificationLink}</p>
|
|
347
|
-
`,
|
|
348
|
-
textBody: "",
|
|
349
|
-
});
|
|
274
|
+
const resetLink = `${configs.jwt.front_end_url}/reset-password?token=${encodeURIComponent(resetToken)}`;
|
|
275
|
+
|
|
276
|
+
await enqueueEmail({ type: 'reset-password', email, resetLink });
|
|
277
|
+
return null;
|
|
350
278
|
};
|
|
351
279
|
|
|
352
280
|
const reset_password_using_token_into_db = async (req: Request) => {
|
|
353
|
-
const token
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
configs.jwt.verified_token as string,
|
|
360
|
-
);
|
|
361
|
-
} catch (error: any) {
|
|
362
|
-
if (error?.message == "invalid signature") {
|
|
363
|
-
throw new AppError("Invalid Token", 403);
|
|
364
|
-
} else if (error?.message == "jwt expired") {
|
|
365
|
-
throw new AppError("Link expired, please reset again", 403);
|
|
366
|
-
}
|
|
281
|
+
const { token, newPass }: { token: string; newPass: string } = req.body;
|
|
282
|
+
const invalid = new AppError('Invalid or expired reset link', 401);
|
|
283
|
+
|
|
284
|
+
const decoded = jwtHelpers.verifyToken<ResetTokenPayload>(token, configs.jwt.reset_secret);
|
|
285
|
+
if (decoded.purpose !== 'reset-password' || typeof decoded.tv !== 'number') {
|
|
286
|
+
throw invalid;
|
|
367
287
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
288
|
+
|
|
289
|
+
// Matching on the token version makes the link single-use (the update bumps it)
|
|
290
|
+
// and also signs out every existing session. Following the emailed link proves
|
|
291
|
+
// ownership of the address, so the account counts as verified from now on.
|
|
292
|
+
const result = await prisma.account.updateMany({
|
|
293
|
+
where: { id: decoded.accountId, isDeleted: false, tokenVersion: decoded.tv },
|
|
294
|
+
data: {
|
|
295
|
+
password: await bcrypt.hash(newPass, BCRYPT_ROUNDS),
|
|
296
|
+
tokenVersion: { increment: 1 },
|
|
297
|
+
isAccountVerified: true,
|
|
298
|
+
lastOtp: null,
|
|
299
|
+
otpAttempts: 0,
|
|
372
300
|
},
|
|
373
301
|
});
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
throw new AppError("Account not found", 404);
|
|
302
|
+
if (!result.count) {
|
|
303
|
+
throw invalid;
|
|
377
304
|
}
|
|
305
|
+
await prisma.refreshToken.deleteMany({ where: { accountId: decoded.accountId } });
|
|
306
|
+
return null;
|
|
307
|
+
};
|
|
378
308
|
|
|
379
|
-
|
|
380
|
-
|
|
309
|
+
/** Revokes every access and refresh token issued for the account. */
|
|
310
|
+
const sign_out_everywhere_from_db = async (req: Request) => {
|
|
311
|
+
const accountId = req.user!.accountId;
|
|
381
312
|
await prisma.account.update({
|
|
382
|
-
where: {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
313
|
+
where: { id: accountId },
|
|
314
|
+
data: { tokenVersion: { increment: 1 } },
|
|
315
|
+
});
|
|
316
|
+
await prisma.refreshToken.deleteMany({ where: { accountId } });
|
|
317
|
+
return null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/** Ends the session the refresh token belongs to (this device only). */
|
|
321
|
+
const sign_out_from_db = async (req: Request) => {
|
|
322
|
+
const token = readRefreshToken(req);
|
|
323
|
+
if (!token) return null;
|
|
324
|
+
const row = await prisma.refreshToken.findUnique({
|
|
325
|
+
where: { tokenHash: hashRefreshToken(token) },
|
|
326
|
+
select: { familyId: true },
|
|
327
|
+
});
|
|
328
|
+
if (row) await prisma.refreshToken.deleteMany({ where: { familyId: row.familyId } });
|
|
329
|
+
return null;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Validates and uses up a refresh token. Returns the user and the session it
|
|
334
|
+
* belongs to, so the controller can issue the next token of that session.
|
|
335
|
+
*/
|
|
336
|
+
const refresh_session_from_db = async (
|
|
337
|
+
req: Request,
|
|
338
|
+
): Promise<{ user: TokenUser; familyId: string }> => {
|
|
339
|
+
const invalid = new AppError('Invalid or expired refresh token', 401);
|
|
340
|
+
const token = readRefreshToken(req);
|
|
341
|
+
if (!token) throw invalid;
|
|
342
|
+
|
|
343
|
+
const tokenHash = hashRefreshToken(token);
|
|
344
|
+
const row = await prisma.refreshToken.findUnique({
|
|
345
|
+
where: { tokenHash },
|
|
346
|
+
include: {
|
|
347
|
+
account: {
|
|
348
|
+
select: { id: true, email: true, role: true, tokenVersion: true, isDeleted: true },
|
|
349
|
+
},
|
|
387
350
|
},
|
|
388
351
|
});
|
|
389
|
-
|
|
390
|
-
|
|
352
|
+
if (!row) throw invalid;
|
|
353
|
+
|
|
354
|
+
const endSession = () => prisma.refreshToken.deleteMany({ where: { familyId: row.familyId } });
|
|
355
|
+
|
|
356
|
+
if (row.usedAt) {
|
|
357
|
+
// An already rotated token came back. Outside the grace window that means two
|
|
358
|
+
// parties hold this session (the token was stolen), so end the whole session.
|
|
359
|
+
if (Date.now() - row.usedAt.getTime() > REFRESH_REUSE_GRACE_MS) await endSession();
|
|
360
|
+
throw invalid;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const isStale =
|
|
364
|
+
row.expiresAt.getTime() < Date.now() ||
|
|
365
|
+
row.account.isDeleted ||
|
|
366
|
+
row.account.tokenVersion !== row.tokenVersion; // password changed / signed out everywhere
|
|
367
|
+
if (isStale) {
|
|
368
|
+
await endSession();
|
|
369
|
+
throw invalid;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// mark it used atomically, so of two parallel refreshes only one gets a new token
|
|
373
|
+
const used = await prisma.refreshToken.updateMany({
|
|
374
|
+
where: { tokenHash, usedAt: null },
|
|
375
|
+
data: { usedAt: new Date() },
|
|
376
|
+
});
|
|
377
|
+
if (!used.count) throw invalid;
|
|
378
|
+
|
|
379
|
+
return { user: toTokenUser(row.account), familyId: row.familyId };
|
|
391
380
|
};
|
|
381
|
+
|
|
382
|
+
/** Trades the single-use code from a social login redirect for the user to sign in. */
|
|
383
|
+
const exchange_social_login_code_from_db = async (req: Request): Promise<TokenUser> => {
|
|
384
|
+
const invalid = new AppError('Invalid or expired login code', 401);
|
|
385
|
+
|
|
386
|
+
// deleting is what makes the code single-use, even under concurrent requests
|
|
387
|
+
const row = await prisma.socialLoginCode
|
|
388
|
+
.delete({
|
|
389
|
+
where: { codeHash: hashLoginCode(req.body.code) },
|
|
390
|
+
select: {
|
|
391
|
+
expiresAt: true,
|
|
392
|
+
account: {
|
|
393
|
+
select: { id: true, email: true, role: true, tokenVersion: true, isDeleted: true },
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
.catch((err) => {
|
|
398
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') return null;
|
|
399
|
+
throw err;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (!row || row.expiresAt.getTime() < Date.now() || row.account.isDeleted) {
|
|
403
|
+
throw invalid;
|
|
404
|
+
}
|
|
405
|
+
return toTokenUser(row.account);
|
|
406
|
+
};
|
|
407
|
+
|
|
392
408
|
export const account_services = {
|
|
393
409
|
create_account_into_db,
|
|
394
410
|
login_user_into_db,
|
|
@@ -397,6 +413,10 @@ export const account_services = {
|
|
|
397
413
|
verify_account_using_otp_into_db,
|
|
398
414
|
resend_otp_and_verification_link_from_db,
|
|
399
415
|
verify_account_using_link_into_db,
|
|
400
|
-
|
|
401
|
-
reset_password_using_token_into_db
|
|
416
|
+
forget_password_generate_reset_token_from_db,
|
|
417
|
+
reset_password_using_token_into_db,
|
|
418
|
+
sign_out_everywhere_from_db,
|
|
419
|
+
sign_out_from_db,
|
|
420
|
+
refresh_session_from_db,
|
|
421
|
+
exchange_social_login_code_from_db,
|
|
402
422
|
};
|